diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7813e6b53e..69f6ed8c96 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -421,7 +421,7 @@ jobs: linux: needs: eligibility - if: ${{ (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.linux)) && needs.eligibility.outputs.full == 'true' }} + if: ${{ !github.event.repository.fork && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.linux)) && needs.eligibility.outputs.full == 'true' }} runs-on: [self-hosted, linux, x64, anyps5] timeout-minutes: 60 env: diff --git a/.github/workflows/input-mapper.yml b/.github/workflows/input-mapper.yml new file mode 100644 index 0000000000..55120c5b54 --- /dev/null +++ b/.github/workflows/input-mapper.yml @@ -0,0 +1,29 @@ +name: Input mapper + +on: + push: + branches: [main] + pull_request: + branches: [main] + paths: + - '.github/workflows/input-mapper.yml' + - 'tools/input-mapper/**' + - 'core/libs/prx/libScePad/include/**' + - 'core/libs/prx/libSceVideoOut/src/InputMapping.cpp' + +permissions: + contents: read + +jobs: + input-mapper: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: actions/setup-node@v7 + with: + node-version: '22' + package-manager-cache: false + - run: npm --prefix tools/input-mapper test diff --git a/.github/workflows/pages-progress.yml b/.github/workflows/pages-progress.yml index 88c1f81d5a..5aa5f304a6 100644 --- a/.github/workflows/pages-progress.yml +++ b/.github/workflows/pages-progress.yml @@ -18,6 +18,7 @@ concurrency: jobs: progress: + if: ${{ !github.event.repository.fork }} runs-on: ubuntu-latest timeout-minutes: 5 steps: diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 0dc28284b0..bf8d6a0313 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -33,7 +33,7 @@ concurrency: jobs: pages: - if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' + if: ${{ !github.event.repository.fork && (github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success') }} runs-on: ubuntu-latest environment: name: github-pages diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index 8de6e8d91f..4c42ec63a0 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -36,4 +36,4 @@ jobs: node --check demo/web/output-directory.mjs node --check demo/web/worker.mjs - name: Test browser converter helpers - run: node --test demo/web/tests/batch.test.mjs demo/web/tests/output-directory.test.mjs + run: node --test demo/web/tests/app.test.mjs demo/web/tests/batch.test.mjs demo/web/tests/output-directory.test.mjs diff --git a/.gitmodules b/.gitmodules index dcde427ae7..219621ad1a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ +[submodule "3rdparty/jxrlib"] + path = 3rdparty/jxrlib + url = https://github.com/4creators/jxrlib.git [submodule "3rdparty/SDL2"] path = 3rdparty/SDL2 url = https://github.com/libsdl-org/SDL.git @@ -61,3 +64,6 @@ [submodule "3rdparty/vorbis"] path = 3rdparty/vorbis url = https://github.com/xiph/vorbis.git +[submodule "3rdparty/nghttp2"] + path = 3rdparty/nghttp2 + url = https://github.com/nghttp2/nghttp2.git diff --git a/3rdparty/SPIRV-Headers b/3rdparty/SPIRV-Headers index 01e0577914..b824a462d4 160000 --- a/3rdparty/SPIRV-Headers +++ b/3rdparty/SPIRV-Headers @@ -1 +1 @@ -Subproject commit 01e0577914a75a2569c846778c2f93aa8e6feddd +Subproject commit b824a462d4256d720bebb40e78b9eb8f78bbb305 diff --git a/3rdparty/SPIRV-Tools b/3rdparty/SPIRV-Tools index 7f2d9ee926..262bdab481 160000 --- a/3rdparty/SPIRV-Tools +++ b/3rdparty/SPIRV-Tools @@ -1 +1 @@ -Subproject commit 7f2d9ee926f98fc77a3ed1e1e0f113b8c9c49458 +Subproject commit 262bdab48146c937467f826699a40da0fdfc0f1a diff --git a/3rdparty/jxrlib b/3rdparty/jxrlib new file mode 160000 index 0000000000..f752187986 --- /dev/null +++ b/3rdparty/jxrlib @@ -0,0 +1 @@ +Subproject commit f7521879862b9085318e814c6157490dd9dbbdb4 diff --git a/3rdparty/nghttp2 b/3rdparty/nghttp2 new file mode 160000 index 0000000000..85e300c79f --- /dev/null +++ b/3rdparty/nghttp2 @@ -0,0 +1 @@ +Subproject commit 85e300c79fb6dbcfa9c1013215c8710c1c2cd3d2 diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d82cc1bec..1ed3029913 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -136,10 +136,17 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/test_shader_cache_native_paths.py ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ShaderCacheVersion.cmake ${CMAKE_COMMAND}) set_tests_properties(shader_cache_native_paths PROPERTIES TIMEOUT 20) - add_test(NAME shader_cache_runtime_sources COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_shader_cache_runtime_sources.py - ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler ${CMAKE_COMMAND}) - set_tests_properties(shader_cache_runtime_sources PROPERTIES TIMEOUT 180) + foreach(shard RANGE 0 6) + if(shard EQUAL 0) + set(shader_cache_sources_test_name shader_cache_runtime_sources) + else() + set(shader_cache_sources_test_name shader_cache_runtime_sources_shard_${shard}) + endif() + add_test(NAME ${shader_cache_sources_test_name} COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_shader_cache_runtime_sources.py + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler ${CMAKE_COMMAND} --shard ${shard}/7) + set_tests_properties(${shader_cache_sources_test_name} PROPERTIES TIMEOUT 180) + endforeach() add_test(NAME pr_overlap_encoding COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_encoding.py) add_test(NAME pr_overlap_dependencies COMMAND ${Python3_EXECUTABLE} @@ -164,6 +171,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_import_audit.py) add_test(NAME unwrap_self COMMAND ${Python3_EXECUTABLE} ${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) foreach(shard RANGE 0 11) if(shard EQUAL 0) set(conventions_test_name conventions_function_bodies) @@ -241,8 +250,19 @@ if(BUILD_TESTING) 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) + set(check_compile_modes_source "${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile_modes.py") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${check_compile_modes_source}") + file(STRINGS "${check_compile_modes_source}" check_compile_modes_cases REGEX "^ def test_") + foreach(test_case IN LISTS check_compile_modes_cases) + string(REGEX REPLACE "^ def (test_[A-Za-z0-9_]+).*" "\\1" test_case "${test_case}") + add_test(NAME check_compile_modes_${test_case} COMMAND ${Python3_EXECUTABLE} + "${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile_modes.py" CheckCompileModeTests.${test_case}) + set_tests_properties(check_compile_modes_${test_case} PROPERTIES TIMEOUT 30) + endforeach() add_test(NAME trace_digest COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py) + add_test(NAME symbolize_samples COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_symbolize_samples.py) add_test(NAME conventions_bom_shebang COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_bom_shebang.py) set_tests_properties(conventions_bom_shebang PROPERTIES TIMEOUT 20) @@ -264,10 +284,17 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_blob_reads.py) 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} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_alignment.py) add_test(NAME conventions_pr_template COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_pr_template.py) + add_test(NAME check_compile_preprocessor COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile_preprocessor.py) + set_tests_properties(check_compile_preprocessor PROPERTIES TIMEOUT 20) add_test(NAME check_compile COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile.py) + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile.py CheckCompileTests) + add_test(NAME check_compile_outputs COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile.py CheckCompileOutputTests) add_test(NAME performance_compare COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_performance_compare.py) set_tests_properties(performance_compare PROPERTIES TIMEOUT 20) @@ -348,6 +375,8 @@ if(NOT ANYPS5_USE_SYSTEM_DEPS) endif() endif() +include(cmake/Jxr.cmake) + set(LIBUSB_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) set(LIBUSB_INSTALL_TARGETS OFF CACHE BOOL "" FORCE) set(LIBUSB_BUILD_TESTING OFF CACHE BOOL "" FORCE) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ef4bf3ea9..d96b4d4c11 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,6 +12,7 @@ - Implement the general behaviour of a function, not what one title happens to need. - Don't add replacements for modules that titles ship themselves in `sce_module/`, `sce_modules/` or `prx/`: engine or middleware modules (Cohtml, FMOD, GOG Galaxy) and SDK libraries that only wrap other system libraries. The relinker converts and loads the title's own module, and a host library with the same name is left out of `DT_NEEDED`. Only system libraries, which reach the kernel or the hardware, are reimplemented in [core/libs/prx](core/libs/prx). A host copy may still exist for a wrapped library that some dumps lack entirely, like libSceNpCppWebApi: the title's own module wins whenever one is shipped, so it is only ever consulted by titles that ship nothing. - Avoid non-standard extensions (`__attribute__`, etc.) where standard C++ is enough. Helper symbols that must not become NIDs use the `_nid_no_patch` or `_nid_no_patch_cut` suffix. +- `alignas` takes at most 8192: MinGW GCC, the Windows compiler, rejects static objects aligned above it. A larger block is aligned at run time inside a bigger buffer. - The relinker uses only the C++20 standard library. - Third-party code is added as a submodule under `3rdparty/` and built from source, not found on the system. With `ANYPS5_USE_SYSTEM_DEPS=ON` the packages packaged by the distributions (SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools) come from the system instead; that resolution lives in [cmake/SystemDeps.cmake](cmake/SystemDeps.cmake) and the default build never reads the system. - Don't add tests that only check that a symbol is exported: a missing export already fails at startup. diff --git a/cmake/Jxr.cmake b/cmake/Jxr.cmake new file mode 100644 index 0000000000..9e20294e31 --- /dev/null +++ b/cmake/Jxr.cmake @@ -0,0 +1,44 @@ +enable_language(C) +set(JXR_SOURCE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/jxrlib) + +function(configure_jxr_native target) + target_include_directories(${target} SYSTEM PRIVATE + ${JXR_SOURCE_DIR}/common/include ${JXR_SOURCE_DIR}/image/sys ${JXR_SOURCE_DIR}/jxrgluelib) + target_compile_definitions(${target} PRIVATE DISABLE_PERF_MEASUREMENT) + if(WIN32) + target_compile_definitions(${target} PRIVATE WIN32 __in=) + target_compile_options(${target} PRIVATE -include ${JXR_SOURCE_DIR}/image/sys/strcodec.h) + else() + target_compile_definitions(${target} PRIVATE __ANSI__) + target_compile_options(${target} PRIVATE -include ${CMAKE_SOURCE_DIR}/cmake/JxrCompat.h) + endif() + set_target_properties(${target} PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON + POSITION_INDEPENDENT_CODE ON C_VISIBILITY_PRESET hidden) +endfunction() + +add_library(jxr STATIC EXCLUDE_FROM_ALL + ${JXR_SOURCE_DIR}/image/sys/adapthuff.c + ${JXR_SOURCE_DIR}/image/sys/image.c + ${JXR_SOURCE_DIR}/image/sys/strcodec.c + ${JXR_SOURCE_DIR}/image/sys/strPredQuant.c + ${JXR_SOURCE_DIR}/image/sys/strTransform.c + ${JXR_SOURCE_DIR}/image/decode/decode.c + ${JXR_SOURCE_DIR}/image/decode/JXRTranscode.c + ${JXR_SOURCE_DIR}/image/decode/postprocess.c + ${JXR_SOURCE_DIR}/image/decode/segdec.c + ${JXR_SOURCE_DIR}/image/decode/strdec.c + ${JXR_SOURCE_DIR}/image/decode/strInvTransform.c + ${JXR_SOURCE_DIR}/image/decode/strPredQuantDec.c + ${JXR_SOURCE_DIR}/image/encode/encode.c + ${JXR_SOURCE_DIR}/image/encode/segenc.c + ${JXR_SOURCE_DIR}/image/encode/strenc.c + ${JXR_SOURCE_DIR}/image/encode/strFwdTransform.c + ${JXR_SOURCE_DIR}/image/encode/strPredQuantEnc.c + ${JXR_SOURCE_DIR}/jxrgluelib/JXRGlue.c + ${JXR_SOURCE_DIR}/jxrgluelib/JXRGlueJxr.c + ${JXR_SOURCE_DIR}/jxrgluelib/JXRGluePFC.c + ${JXR_SOURCE_DIR}/jxrgluelib/JXRMeta.c) +configure_jxr_native(jxr) +if(UNIX) + target_link_libraries(jxr PRIVATE m) +endif() diff --git a/cmake/JxrCompat.h b/cmake/JxrCompat.h new file mode 100644 index 0000000000..4268ed9a1f --- /dev/null +++ b/cmake/JxrCompat.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include "strcodec.h" + +#if defined(__ANSI__) && !defined(WIN32) && !defined(_BIG__ENDIAN_) +U32 _byteswap_ulong(U32 bits); +#endif diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 728bf2a869..2372b69617 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -269,6 +269,15 @@ if(BUILD_TESTING) set_tests_properties(guest_kernel_sys_sdk_version PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_kernel_sys_console_identity_tests tests/GuestKernelSysConsoleIdentity.cpp) + target_include_directories(guest_kernel_sys_console_identity_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_console_identity_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_console_identity_tests) + add_test(NAME guest_kernel_sys_console_identity COMMAND guest_kernel_sys_console_identity_tests) + if(WIN32) + set_tests_properties(guest_kernel_sys_console_identity PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(nid_buffer_validation_tests nid/tests/BufferValidation.cpp) target_include_directories(nid_buffer_validation_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/include) target_compile_features(nid_buffer_validation_tests PRIVATE cxx_std_20) @@ -278,6 +287,32 @@ if(BUILD_TESTING) add_test(NAME nid_buffer_validation COMMAND nid_buffer_validation_tests) set_tests_properties(nid_buffer_validation PROPERTIES TIMEOUT 10) + add_test_executable(guest_dinkumware_fetch_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/GuestDinkumwareFetch.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/nid/src/NidCompute.cpp ${CMAKE_CURRENT_SOURCE_DIR}/nid/src/Sha1.cpp) + target_include_directories(guest_dinkumware_fetch_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/nid/include) + target_link_libraries(guest_dinkumware_fetch_tests PRIVATE ${CMAKE_DL_LIBS}) + configure_windows_unwind(guest_dinkumware_fetch_tests) + if(MINGW) + target_link_libraries(guest_dinkumware_fetch_tests PRIVATE libc) + endif() + add_custom_target(guest_dinkumware_fetch_libraries DEPENDS ${LIBC_PATCHED}) + add_dependencies(guest_dinkumware_fetch_tests guest_dinkumware_fetch_libraries) + foreach(operation IN ITEMS and or xor) + foreach(width IN ITEMS 1 2 4) + add_test(NAME guest_dinkumware_fetch_${operation}_${width} COMMAND guest_dinkumware_fetch_tests + $ unpatched ${operation} ${width}) + add_test(NAME guest_dinkumware_fetch_${operation}_${width}_patched COMMAND guest_dinkumware_fetch_tests + ${PATCHED_DIR} patched ${operation} ${width}) + set_tests_properties(guest_dinkumware_fetch_${operation}_${width} + guest_dinkumware_fetch_${operation}_${width}_patched PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_dinkumware_fetch_${operation}_${width} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(guest_dinkumware_fetch_${operation}_${width}_patched PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:${PATCHED_DIR}") + endif() + endforeach() + endforeach() add_test_executable(pe_import_first_tests allocator/tests/PeImportFirstTests.cpp allocator/PeImportFirst.cpp) target_include_directories(pe_import_first_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_compile_features(pe_import_first_tests PRIVATE cxx_std_20) @@ -329,6 +364,11 @@ if(BUILD_TESTING) target_link_options(patched_libraries_tests PRIVATE -static -static-libgcc -static-libstdc++) endif() add_test(NAME patched_libraries COMMAND patched_libraries_tests ${PATCHED_DIR}) + if(MINGW) + get_filename_component(patchedLibrariesRuntimeDirectory "${CMAKE_CXX_COMPILER}" DIRECTORY) + set_tests_properties(patched_libraries PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:${patchedLibrariesRuntimeDirectory}") + endif() if(CMAKE_SYSTEM_NAME STREQUAL "Linux") add_library(host_libstdcxx_exception_module SHARED tests/HostLibstdcxxExceptionModule.cpp) target_link_libraries(host_libstdcxx_exception_module PRIVATE ${PATCHED_DIR}/libc.prx) @@ -366,14 +406,52 @@ if(BUILD_TESTING) target_link_libraries(guest_kernel_sys_pthread_tests PRIVATE libkernel_sys libkernel libc) configure_windows_unwind(guest_kernel_sys_pthread_tests) add_test(NAME guest_kernel_sys_pthread COMMAND guest_kernel_sys_pthread_tests) + add_test_executable(guest_kernel_sys_constants_tests tests/GuestKernelSysConstants.cpp) + target_include_directories(guest_kernel_sys_constants_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_constants_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_constants_tests) + add_test(NAME guest_kernel_sys_constants COMMAND guest_kernel_sys_constants_tests) + set_tests_properties(guest_kernel_sys_constants PROPERTIES TIMEOUT 20) + add_test_executable(guest_firmware_constant_returns_tests tests/GuestFirmwareConstantReturns.cpp) + target_include_directories(guest_firmware_constant_returns_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_firmware_constant_returns_tests PRIVATE libSceComposite libSceFsInternalForVsh libSceHttpCache libScePlayReady libSceSdma libSceSsl libSceVisionManager libmdbg_syscore libkernel libc) + configure_windows_unwind(guest_firmware_constant_returns_tests) + add_test(NAME guest_firmware_constant_returns COMMAND guest_firmware_constant_returns_tests) + set_tests_properties(guest_firmware_constant_returns PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_firmware_constant_returns PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$;PATH=path_list_prepend:$;PATH=path_list_prepend:$;PATH=path_list_prepend:$;PATH=path_list_prepend:$;PATH=path_list_prepend:$;PATH=path_list_prepend:$;PATH=path_list_prepend:$") + endif() if(Python3_Interpreter_FOUND) add_test(NAME nid_verneed COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_verneed.py $) endif() if(WIN32) - set_tests_properties(guest_kernel_sys guest_kernel_sys_thread_defaults guest_kernel_sys_barrier guest_kernel_sys_pthread PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_kernel_sys guest_kernel_sys_thread_defaults guest_kernel_sys_barrier guest_kernel_sys_pthread guest_kernel_sys_constants PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_kernel_sys_threads_tests tests/GuestKernelSysThreads.cpp + nid/src/NidCompute.cpp nid/src/Sha1.cpp) + target_include_directories(guest_kernel_sys_threads_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/nid/include) + target_link_libraries(guest_kernel_sys_threads_tests PRIVATE ${CMAKE_DL_LIBS}) + add_custom_target(guest_kernel_sys_threads_libraries DEPENDS + ${PATCHED_DIR}/libkernel_sys.prx ${PATCHED_DIR}/libkernel.prx ${LIBC_PATCHED}) + add_dependencies(guest_kernel_sys_threads_tests guest_kernel_sys_threads_libraries) + configure_windows_unwind(guest_kernel_sys_threads_tests) + if(MINGW) + target_compile_options(guest_kernel_sys_threads_tests PRIVATE -fasynchronous-unwind-tables) + target_link_options(guest_kernel_sys_threads_tests PRIVATE -static -static-libgcc -static-libstdc++) + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test(NAME guest_kernel_sys_threads_main_late_load COMMAND guest_kernel_sys_threads_tests ${UNPATCHED_DIR} unpatched main_late_load) + add_test(NAME guest_kernel_sys_threads_main_late_load_patched COMMAND guest_kernel_sys_threads_tests ${PATCHED_DIR} patched main_late_load) + set_tests_properties(guest_kernel_sys_threads_main_late_load guest_kernel_sys_threads_main_late_load_patched PROPERTIES TIMEOUT 20) + endif() + foreach(case IN ITEMS main) + add_test(NAME guest_kernel_sys_threads_${case} COMMAND guest_kernel_sys_threads_tests ${UNPATCHED_DIR} unpatched ${case}) + add_test(NAME guest_kernel_sys_threads_${case}_patched COMMAND guest_kernel_sys_threads_tests ${PATCHED_DIR} patched ${case}) + set_tests_properties(guest_kernel_sys_threads_${case} guest_kernel_sys_threads_${case}_patched PROPERTIES TIMEOUT 20) + endforeach() add_test_executable(guest_gnm_neo_packets_tests tests/GuestGnmNeoPackets.cpp) target_include_directories(guest_gnm_neo_packets_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) @@ -388,6 +466,18 @@ if(BUILD_TESTING) add_subdirectory(tests/write_watch) add_subdirectory(tests/guest_arena) endif() + add_test_executable(guest_kernel_sys_pthread_np_tests tests/GuestKernelSysPthreadNp.cpp) + target_include_directories(guest_kernel_sys_pthread_np_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_pthread_np_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_pthread_np_tests) + foreach(pthreadNpMode IN ITEMS rwlockattr kind) + add_test(NAME guest_kernel_sys_pthread_np_${pthreadNpMode} COMMAND guest_kernel_sys_pthread_np_tests ${pthreadNpMode}) + set_tests_properties(guest_kernel_sys_pthread_np_${pthreadNpMode} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_kernel_sys_pthread_np_${pthreadNpMode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() add_test_executable(guest_kernel_sys_module_internal_tests tests/GuestKernelSysModuleInternal.cpp) target_include_directories(guest_kernel_sys_module_internal_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_kernel_sys_module_internal_tests PRIVATE libkernel_sys libkernel libc) @@ -407,6 +497,23 @@ if(BUILD_TESTING) endif() add_test(NAME windows_write_watch COMMAND windows_write_watch_tests) set_tests_properties(windows_write_watch PROPERTIES TIMEOUT 20) + add_test_executable(windows_shared_map_cost_tests tests/WindowsSharedMapCost.cpp) + target_include_directories(windows_shared_map_cost_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_compile_definitions(windows_shared_map_cost_tests PRIVATE _WIN32_WINNT=0x0A00) + if(MINGW) + target_compile_options(windows_shared_map_cost_tests PRIVATE -fasynchronous-unwind-tables) + endif() + add_test(NAME windows_shared_map_cost COMMAND windows_shared_map_cost_tests) + set_tests_properties(windows_shared_map_cost PROPERTIES TIMEOUT 120) + endif() + add_test_executable(guest_kernel_sys_login_tests tests/GuestKernelSysLogin.cpp) + target_include_directories(guest_kernel_sys_login_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_login_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_login_tests) + add_test(NAME guest_kernel_sys_login COMMAND guest_kernel_sys_login_tests) + if(WIN32) + set_tests_properties(guest_kernel_sys_login PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() if(WIN32) @@ -489,6 +596,17 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_create_suspended_tests tests/GuestPthreadCreateSuspended.cpp) + target_include_directories(guest_pthread_create_suspended_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_pthread_create_suspended_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_pthread_create_suspended_tests) + add_test(NAME guest_pthread_create_suspended COMMAND guest_pthread_create_suspended_tests) + set_tests_properties(guest_pthread_create_suspended PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_create_suspended PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_np_party_tests tests/GuestNpParty.cpp) target_include_directories(guest_np_party_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_np_party_tests PRIVATE libSceNpParty libc) @@ -859,6 +977,18 @@ if(BUILD_TESTING) endif() add_test(NAME MemoryPoolTest COMMAND MemoryPoolTest) + add_test_executable(guest_kernel_sys_swapcontext_tests tests/GuestKernelSysSwapcontext.cpp) + target_include_directories(guest_kernel_sys_swapcontext_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_compile_options(guest_kernel_sys_swapcontext_tests PRIVATE -fno-omit-frame-pointer) + target_link_libraries(guest_kernel_sys_swapcontext_tests PRIVATE libkernel_sys libSceLibcInternal libkernel libc) + configure_windows_unwind(guest_kernel_sys_swapcontext_tests) + add_test(NAME guest_kernel_sys_swapcontext COMMAND guest_kernel_sys_swapcontext_tests) + set_tests_properties(guest_kernel_sys_swapcontext PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_kernel_sys_swapcontext PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_acm_tests tests/GuestAcm.cpp) target_include_directories(guest_acm_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_acm_tests PRIVATE libSceAcm libc) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index f51297c09b..0eb23cef6d 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1502,6 +1502,15 @@ struct SharePlayConnectionInfoA { std::int32_t visitorUserId; }; static_assert(sizeof(SharePlayConnectionInfoA) == 72 && offsetof(SharePlayConnectionInfoA, hostAccountId) == 48 && offsetof(SharePlayConnectionInfoA, hostUserId) == 64); +struct SharePlayConnectionInfo { + std::int32_t status; + std::int32_t mode; + NpOnlineId hostOnlineId; + NpOnlineId visitorOnlineId; + std::int32_t hostUserId; + std::int32_t visitorUserId; +}; +static_assert(sizeof(SharePlayConnectionInfo) == 56 && offsetof(SharePlayConnectionInfo, hostUserId) == 48); struct NpId { NpOnlineId online_id; std::uint8_t opaque[4]; }; struct NpCreateAsyncRequestParameter { std::uint8_t opaque[64]; }; struct NpCheckPremiumParameter { std::uint8_t opaque[64]; }; diff --git a/core/libs/nid/include/nid/ElfPatcher.hpp b/core/libs/nid/include/nid/ElfPatcher.hpp index 0415b4eb7f..f6887aae64 100644 --- a/core/libs/nid/include/nid/ElfPatcher.hpp +++ b/core/libs/nid/include/nid/ElfPatcher.hpp @@ -22,6 +22,17 @@ struct Elf64_Ehdr { std::uint16_t e_shstrndx; }; +struct Elf64_Phdr { + std::uint32_t p_type; + std::uint32_t p_flags; + std::uint64_t p_offset; + std::uint64_t p_vaddr; + std::uint64_t p_paddr; + std::uint64_t p_filesz; + std::uint64_t p_memsz; + std::uint64_t p_align; +}; + struct Elf64_Shdr { std::uint32_t sh_name; std::uint32_t sh_type; @@ -103,7 +114,7 @@ constexpr std::uint16_t kShnUndef = 0u; class ElfNidPatcher final : public IBinaryPatcher { public: - void PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports) const override; + void PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports, const PatchOptions& options = {}) const override; }; } diff --git a/core/libs/nid/include/nid/IBinaryPatcher.hpp b/core/libs/nid/include/nid/IBinaryPatcher.hpp index 0fbcb88501..4e8cff5cbc 100644 --- a/core/libs/nid/include/nid/IBinaryPatcher.hpp +++ b/core/libs/nid/include/nid/IBinaryPatcher.hpp @@ -8,11 +8,16 @@ namespace Nid { +struct PatchOptions { + bool executable = false; + bool onlySce = false; +}; + class IBinaryPatcher { public: virtual ~IBinaryPatcher() = default; - virtual void PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports) const = 0; + virtual void PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports, const PatchOptions& options = {}) const = 0; }; } diff --git a/core/libs/nid/include/nid/MachONidPatcher.hpp b/core/libs/nid/include/nid/MachONidPatcher.hpp index f49732cd88..f6ae370549 100644 --- a/core/libs/nid/include/nid/MachONidPatcher.hpp +++ b/core/libs/nid/include/nid/MachONidPatcher.hpp @@ -22,7 +22,7 @@ std::vector ReadMachOExports(const std::vector& binar class MachONidPatcher final : public IBinaryPatcher { public: - void PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports) const override; + void PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports, const PatchOptions& options = {}) const override; }; } diff --git a/core/libs/nid/include/nid/NidResolver.hpp b/core/libs/nid/include/nid/NidResolver.hpp index df144b4519..9822939e4e 100644 --- a/core/libs/nid/include/nid/NidResolver.hpp +++ b/core/libs/nid/include/nid/NidResolver.hpp @@ -1,6 +1,7 @@ #ifndef NID_NIDRESOLVER_HPP #define NID_NIDRESOLVER_HPP +#include #include #include #include @@ -8,6 +9,10 @@ namespace Nid { +bool IsSceImport(const std::string& name); + +std::unordered_map ResolveImportNids(const std::vector& names, const std::unordered_set& excluded, const PatchOptions& options); + std::string ResolveOneName(const std::string& funcName); std::unordered_map ResolveNids(const std::vector& exportedNames, const std::string& libraryName, const std::unordered_set& excludedExports); diff --git a/core/libs/nid/include/nid/PeNidPatcher.hpp b/core/libs/nid/include/nid/PeNidPatcher.hpp index c674c3a80b..d9d14bda4b 100644 --- a/core/libs/nid/include/nid/PeNidPatcher.hpp +++ b/core/libs/nid/include/nid/PeNidPatcher.hpp @@ -39,7 +39,7 @@ struct PeSectionHeader { class PeNidPatcher final : public IBinaryPatcher { public: - void PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports) const override; + void PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports, const PatchOptions& options = {}) const override; }; } diff --git a/core/libs/nid/src/ElfNidPatcher.cpp b/core/libs/nid/src/ElfNidPatcher.cpp index 089bcd7107..2e06bc5431 100644 --- a/core/libs/nid/src/ElfNidPatcher.cpp +++ b/core/libs/nid/src/ElfNidPatcher.cpp @@ -354,7 +354,7 @@ std::vector CollectVerneedNameOffsets(const std::vector& elf, const std::string& libraryName, const std::unordered_set& excludedExports) const { +void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& libraryName, const std::unordered_set& excludedExports, const PatchOptions& options) const { using namespace Internal; if (elf.size() < sizeof(Elf64_Ehdr)) throw std::runtime_error("file too small"); @@ -370,11 +370,13 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& std::size_t sysvHashOffset = 0u, sysvHashSize = 0u; std::uint32_t sysvHashLink = 0u; std::size_t versymOffset = 0u; + bool hasDynamic = false; std::uint32_t dynSymSectionIndex = 0u; std::uint32_t gnuHashSymOffset = 0u; for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) { const std::size_t shOffset = static_cast(ehdr.e_shoff) + i * sizeof(Elf64_Shdr); const auto shdr = Read(elf, shOffset); + if (shdr.sh_type == kShtDynamic) hasDynamic = true; if (shdr.sh_type == kShtDynsym) { if (shdr.sh_entsize != sizeof(Elf64_Sym) || shdr.sh_size % sizeof(Elf64_Sym) || shdr.sh_link >= ehdr.e_shnum) throw std::runtime_error("invalid ELF dynsym layout"); @@ -398,6 +400,19 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& throw std::runtime_error("unsupported .gnu.version_d section: symbol version definitions cannot be preserved"); } + if (options.executable && dynSymOffset == 0u && !hasDynamic) { + if (ehdr.e_phnum) { + if (!ehdr.e_phoff || ehdr.e_phentsize != sizeof(Elf64_Phdr) || ehdr.e_phnum == 0xffffu) + throw std::runtime_error("unsupported ELF program header layout"); + RequireElfRange(elf, ehdr.e_phoff, std::uint64_t{ehdr.e_phnum} * sizeof(Elf64_Phdr), "program headers"); + for (std::uint16_t index = 0u; index < ehdr.e_phnum; ++index) { + const auto segment = Read(elf, ehdr.e_phoff + index * sizeof(Elf64_Phdr)); + if (segment.p_type == 2u) + throw std::runtime_error("unsupported ELF PT_DYNAMIC without a .dynsym section"); + } + } + return; + } if (dynSymOffset == 0u) throw std::runtime_error("no .dynsym section"); const std::size_t symCount = dynSymSize / sizeof(Elf64_Sym); @@ -437,19 +452,26 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& elf.begin() + static_cast(dynStrOffset + dynStrSize) ); + std::vector importedNames; std::vector exportedNames; for (std::size_t i = 1u; i < symCount; ++i) { const std::size_t symOffset = dynSymOffset + i * sizeof(Elf64_Sym); const auto sym = Read(elf, symOffset); if (sym.st_name == 0u) continue; const std::uint8_t binding = sym.st_info >> 4u; - if (binding == kStbLocal || sym.st_shndx == kShnUndef) continue; + if (binding == kStbLocal) continue; + if (sym.st_shndx == kShnUndef) { + const auto name = ReadCStr(origDynStr, sym.st_name); + if (!name.empty()) importedNames.push_back(name); + continue; + } const std::string symName = ReadCStr(origDynStr, sym.st_name); if (symName.empty()) continue; exportedNames.push_back(symName); } - const auto nidMap = ResolveNids(exportedNames, libraryName, excludedExports); + const auto nidMap = options.executable ? std::unordered_map{} : ResolveNids(exportedNames, libraryName, excludedExports); + const auto importMap = ResolveImportNids(importedNames, excludedExports, options); struct SymbolNameUse { std::size_t symIndex; @@ -481,14 +503,11 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& } else if (binding != kStbLocal && sym.st_shndx == kShnUndef) { const bool isUnversioned = versymOffset == 0u || (Read(elf, versymOffset + i * sizeof(std::uint16_t)) <= 1u); - const bool hasNidPostfix = symName.size() >= kNidPostfixLen && - symName.compare(symName.size() - kNidPostfixLen, kNidPostfixLen, kNidPostfix) == 0; - const bool hasScePrefix = symName.size() >= 3u && - std::tolower(static_cast(symName[0])) == 's' && - std::tolower(static_cast(symName[1])) == 'c' && - std::tolower(static_cast(symName[2])) == 'e'; - if (isUnversioned && (hasNidPostfix || hasScePrefix)) - newValue = ResolveOneName(symName); + if (options.executable || isUnversioned) { + newValue = importMap.at(symName); + if (options.executable && newValue != symName && versymOffset != 0u) + Write(elf, versymOffset + i * sizeof(std::uint16_t), std::uint16_t{1}); + } } uses.push_back(SymbolNameUse{i, sym.st_name, newValue}); @@ -538,11 +557,69 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& for (const auto offset : preservedOffsets) preservedToNewOffset.emplace_back(offset, valueOffsets.at(ReadCStr(origDynStr, offset))); + if (newDynStr.size() > dynStrSize && options.executable) { + if (ehdr.e_phentsize != sizeof(Elf64_Phdr) || ehdr.e_phnum == 0 || ehdr.e_phnum >= 0xfffdu) + throw std::runtime_error("invalid ELF program header table for string growth"); + RequireElfRange(elf, ehdr.e_phoff, std::uint64_t{ehdr.e_phnum} * sizeof(Elf64_Phdr), "program headers"); + const auto align = [](std::uint64_t value) { + if (value > UINT64_MAX - 4095u) throw std::runtime_error("ELF address overflow"); + return (value + 4095u) & ~std::uint64_t{4095u}; + }; + std::vector segments; + std::uint64_t virtualEnd = 0; + bool hasPhdr = false; + for (std::size_t index = 0; index < ehdr.e_phnum; ++index) { + const auto segment = Read(elf, ehdr.e_phoff + index * sizeof(Elf64_Phdr)); + segments.push_back(segment); + hasPhdr |= segment.p_type == 6; + if (segment.p_type != 1) continue; + if (segment.p_memsz > UINT64_MAX - segment.p_vaddr) throw std::runtime_error("ELF segment address overflow"); + virtualEnd = std::max(virtualEnd, segment.p_vaddr + segment.p_memsz); + } + if (!virtualEnd) throw std::runtime_error("ELF executable has no load segment"); + if (!hasPhdr) segments.insert(segments.begin(), Elf64_Phdr{6, 4, 0, 0, 0, 0, 0, 8}); + const auto fileOffset = align(elf.size()); + const auto address = align(virtualEnd); + const auto tableSize = (segments.size() + 1u) * sizeof(Elf64_Phdr); + const auto segmentSize = tableSize + newDynStr.size(); + if (segmentSize > UINT64_MAX - address || segmentSize > SIZE_MAX - fileOffset) throw std::runtime_error("ELF string segment overflow"); + for (auto& segment : segments) { + if (segment.p_type != 6) continue; + segment = {6, 4, fileOffset, address, address, tableSize, tableSize, 8}; + } + segments.push_back({1, 4, fileOffset, address, address, segmentSize, segmentSize, 4096}); + elf.resize(static_cast(fileOffset + segmentSize), 0); + for (std::size_t index = 0; index < segments.size(); ++index) + Write(elf, fileOffset + index * sizeof(Elf64_Phdr), segments[index]); + Write(elf, offsetof(Elf64_Ehdr, e_phoff), fileOffset); + Write(elf, offsetof(Elf64_Ehdr, e_phnum), static_cast(segments.size())); + dynStrOffset = static_cast(fileOffset + tableSize); + dynStrSize = newDynStr.size(); + const auto stringAddress = address + tableSize; + const auto sectionOffset = ehdr.e_shoff + dynStrSectionLink * sizeof(Elf64_Shdr); + auto strings = Read(elf, sectionOffset); + strings.sh_offset = dynStrOffset; + strings.sh_addr = stringAddress; + strings.sh_size = dynStrSize; + Write(elf, sectionOffset, strings); + bool foundAddress = false; + bool foundSize = false; + for (std::size_t index = 0; index < ehdr.e_shnum; ++index) { + const auto section = Read(elf, ehdr.e_shoff + index * sizeof(Elf64_Shdr)); + if (section.sh_type != kShtDynamic) continue; + RequireElfRange(elf, section.sh_offset, section.sh_size, "dynamic"); + for (std::uint64_t offset = 0; offset + sizeof(Elf64_Dyn) <= section.sh_size; offset += sizeof(Elf64_Dyn)) { + auto entry = Read(elf, section.sh_offset + offset); + if (entry.d_tag == kDtNull) break; + if (entry.d_tag == 5) { entry.d_val = stringAddress; foundAddress = true; } + if (entry.d_tag == 10) { entry.d_val = dynStrSize; foundSize = true; } + Write(elf, section.sh_offset + offset, entry); + } + } + if (!foundAddress || !foundSize) throw std::runtime_error("ELF executable lacks DT_STRTAB or DT_STRSZ"); + } if (newDynStr.size() > dynStrSize) { - throw std::runtime_error( - "rebuilt .dynstr (" + std::to_string(newDynStr.size()) + " bytes) exceeds original section size (" + - std::to_string(dynStrSize) + " bytes) - relinking with a larger section is required" - ); + throw std::runtime_error("rebuilt .dynstr exceeds original section size; relinking with a larger section is required"); } newDynStr.resize(dynStrSize, 0u); diff --git a/core/libs/nid/src/MachONidPatcher.cpp b/core/libs/nid/src/MachONidPatcher.cpp index 45bdee2735..71e8713f27 100644 --- a/core/libs/nid/src/MachONidPatcher.cpp +++ b/core/libs/nid/src/MachONidPatcher.cpp @@ -341,15 +341,6 @@ std::string StripUnderscore(const std::string& name) { return !name.empty() && name[0] == '_' ? name.substr(1) : name; } -bool IsGuestFacingImport(const std::string& name) { - using namespace Internal; - const bool hasNidPostfix = name.size() >= kNidPostfixLen && name.compare(name.size() - kNidPostfixLen, kNidPostfixLen, kNidPostfix) == 0; - const bool hasScePrefix = name.size() >= 3u && - std::tolower(static_cast(name[0])) == 's' && - std::tolower(static_cast(name[1])) == 'c' && - std::tolower(static_cast(name[2])) == 'e'; - return hasNidPostfix || hasScePrefix; -} std::vector DylibOrdinalsArePrx(const std::vector& binary, const Image& image) { std::vector result; @@ -364,11 +355,48 @@ std::vector DylibOrdinalsArePrx(const std::vector& binary, c return result; } -void PatchChainedImports(std::vector& binary, const Image& image) { +void WriteLinkeditData(std::vector& binary, const Image& image, const TrieLocation& location, const std::vector& trie) { + const auto oldOffset = Read(binary, location.OffsetField); + const auto oldSize = Read(binary, location.SizeField); + if (trie.size() <= oldSize) { + std::copy(trie.begin(), trie.end(), binary.begin() + oldOffset); + std::fill(binary.begin() + oldOffset + static_cast(trie.size()), binary.begin() + oldOffset + oldSize, 0); + Write(binary, location.SizeField, static_cast(trie.size())); + return; + } + std::optional linkedit; + for (const auto offset : image.Commands) { + const auto cmd = Read(binary, offset).Cmd; + if (cmd == kLcCodeSignature) throw std::runtime_error("signed Mach-O images cannot grow linkedit data"); + if (cmd != kLcSegment64) continue; + const auto segment = Read(binary, offset); + if (std::strncmp(segment.Name, "__LINKEDIT", sizeof(segment.Name)) == 0) linkedit = offset; + } + if (!linkedit) throw std::runtime_error("Mach-O image has no __LINKEDIT segment"); + auto segment = Read(binary, *linkedit); + if (segment.FileOffset + segment.FileSize != binary.size()) throw std::runtime_error("__LINKEDIT does not end the file"); + const auto newOffset = (binary.size() + 7) & ~std::size_t{7}; + if (newOffset + trie.size() > 0xffffffffu) throw std::runtime_error("linkedit data offset overflow"); + binary.resize(newOffset, 0); + binary.insert(binary.end(), trie.begin(), trie.end()); + std::fill(binary.begin() + oldOffset, binary.begin() + oldOffset + oldSize, 0); + segment.FileSize = binary.size() - segment.FileOffset; + segment.VmSize = std::max(segment.VmSize, (segment.FileSize + kPageSize - 1) & ~(kPageSize - 1)); + Write(binary, *linkedit, segment); + Write(binary, location.OffsetField, static_cast(newOffset)); + Write(binary, location.SizeField, static_cast(trie.size())); +} + +void PatchChainedImports(std::vector& binary, const Image& image, const std::unordered_set& excluded, const PatchOptions& options) { + std::size_t commandOffset = 0; std::optional fixups; for (const auto offset : image.Commands) { const auto cmd = Read(binary, offset).Cmd; - if (cmd == kLcDyldChainedFixups) fixups = Read(binary, offset); + if (cmd == kLcDyldChainedFixups) { + fixups = Read(binary, offset); + commandOffset = offset; + } + if (options.executable && cmd == kLcCodeSignature) throw std::runtime_error("remove the Mach-O code signature before patching and sign again afterwards"); if ((cmd == kLcDyldInfo || cmd == kLcDyldInfoOnly)) { const auto info = Read(binary, offset); if (info.BindSize != 0 || info.LazyBindSize != 0 || info.WeakBindSize != 0) throw std::runtime_error("Mach-O bind opcodes are not supported; link with chained fixups"); @@ -400,61 +428,72 @@ void PatchChainedImports(std::vector& binary, const Image& image) imports.push_back({raw >> 9, raw & 0xff}); } } - std::set nameOffsets; - for (const auto& item : imports) nameOffsets.insert(item.NameOffset); - std::set rewritten; + std::vector names; + std::vector originalNames; + std::vector selected; for (const auto& item : imports) { - const bool special = header.ImportsFormat == kImportFormatAddend64 ? item.Ordinal >= 0xfff0 : item.Ordinal >= 0xf0; - if (item.Ordinal == 0 || special || item.Ordinal > prx.size() || !prx[item.Ordinal - 1]) continue; - if (rewritten.contains(item.NameOffset)) continue; const auto at = symbols + item.NameOffset; if (at >= symbolsEnd) throw std::runtime_error("chained import name out of bounds"); const auto name = Internal::ReadCStr(binary, at); if (at + name.size() >= symbolsEnd) throw std::runtime_error("unterminated chained import name"); const auto bare = StripUnderscore(name); - if (bare == name || !IsGuestFacingImport(bare)) continue; - const auto patched = "_" + ResolveOneName(bare); - if (patched.size() > name.size()) throw std::runtime_error("import NID longer than original name: " + name); - const auto inside = nameOffsets.upper_bound(item.NameOffset); - if (inside != nameOffsets.end() && *inside <= item.NameOffset + name.size()) throw std::runtime_error("chained import names share storage: " + name); - std::memcpy(binary.data() + at, patched.data(), patched.size()); - std::fill(binary.begin() + static_cast(at + patched.size()), binary.begin() + static_cast(at + name.size() + 1), 0); - rewritten.insert(item.NameOffset); + const bool special = header.ImportsFormat == kImportFormatAddend64 ? item.Ordinal >= 0xfff0 : item.Ordinal >= 0xf0; + const bool eligible = options.executable || (bare != name && item.Ordinal != 0 && !special && item.Ordinal <= prx.size() && prx[item.Ordinal - 1]); + originalNames.push_back(name); + selected.push_back(eligible); + if (eligible) names.push_back(bare); } -} - -void WriteTrie(std::vector& binary, const Image& image, const TrieLocation& location, const std::vector& trie) { - const auto oldOffset = Read(binary, location.OffsetField); - const auto oldSize = Read(binary, location.SizeField); - if (trie.size() <= oldSize) { - std::copy(trie.begin(), trie.end(), binary.begin() + oldOffset); - std::fill(binary.begin() + oldOffset + static_cast(trie.size()), binary.begin() + oldOffset + oldSize, 0); - Write(binary, location.SizeField, static_cast(trie.size())); + const auto mapping = ResolveImportNids(names, excluded, options); + if (!options.executable) { + std::set nameOffsets; + for (const auto& item : imports) nameOffsets.insert(item.NameOffset); + std::set rewritten; + for (const auto& item : imports) { + const bool special = header.ImportsFormat == kImportFormatAddend64 ? item.Ordinal >= 0xfff0 : item.Ordinal >= 0xf0; + if (item.Ordinal == 0 || special || item.Ordinal > prx.size() || !prx[item.Ordinal - 1]) continue; + if (rewritten.contains(item.NameOffset)) continue; + const auto at = symbols + item.NameOffset; + if (at >= symbolsEnd) throw std::runtime_error("chained import name out of bounds"); + const auto name = Internal::ReadCStr(binary, at); + if (at + name.size() >= symbolsEnd) throw std::runtime_error("unterminated chained import name"); + const auto bare = StripUnderscore(name); + if (bare == name) continue; + const auto patched = "_" + mapping.at(bare); + if (patched.size() > name.size()) throw std::runtime_error("import NID longer than original name: " + name); + const auto inside = nameOffsets.upper_bound(item.NameOffset); + if (inside != nameOffsets.end() && *inside <= item.NameOffset + name.size()) throw std::runtime_error("chained import names share storage: " + name); + std::memcpy(binary.data() + at, patched.data(), patched.size()); + std::fill(binary.begin() + static_cast(at + patched.size()), binary.begin() + static_cast(at + name.size() + 1), 0); + rewritten.insert(item.NameOffset); + } return; } - std::optional linkedit; - for (const auto offset : image.Commands) { - const auto cmd = Read(binary, offset).Cmd; - if (cmd == kLcCodeSignature) throw std::runtime_error("signed Mach-O images cannot grow their export trie"); - if (cmd != kLcSegment64) continue; - const auto segment = Read(binary, offset); - if (std::strncmp(segment.Name, "__LINKEDIT", sizeof(segment.Name)) == 0) linkedit = offset; + if (imports.empty()) return; + std::vector data(binary.begin() + base, binary.begin() + symbolsEnd); + const auto newSymbols = data.size(); + if (newSymbols > 0xffffffffu) throw std::runtime_error("chained symbol offset overflow"); + Write(data, offsetof(ChainedFixupsHeader, SymbolsOffset), static_cast(newSymbols)); + for (std::size_t index = 0; index < imports.size(); ++index) { + const auto& original = originalNames[index]; + const auto name = selected[index] ? (original.starts_with("_") ? "_" : "") + mapping.at(StripUnderscore(original)) : original; + const auto offset = data.size() - newSymbols; + const auto at = header.ImportsOffset + index * entrySize; + if (header.ImportsFormat == kImportFormatAddend64) { + if (offset > 0xffffffffu) throw std::runtime_error("chained symbol offset overflow"); + const auto raw = Read(data, at); + Write(data, at, (raw & 0xffffffffu) | (std::uint64_t{offset} << 32)); + } else { + if (offset > 0x7fffffu) throw std::runtime_error("chained symbol offset overflow"); + const auto raw = Read(data, at); + Write(data, at, (raw & 0x1ffu) | (static_cast(offset) << 9)); + } + data.insert(data.end(), name.begin(), name.end()); + data.push_back(0); } - if (!linkedit) throw std::runtime_error("Mach-O image has no __LINKEDIT segment"); - auto segment = Read(binary, *linkedit); - if (segment.FileOffset + segment.FileSize != binary.size()) throw std::runtime_error("__LINKEDIT does not end the file"); - const auto newOffset = (binary.size() + 7) & ~std::size_t{7}; - if (newOffset + trie.size() > 0xffffffffu) throw std::runtime_error("export trie offset overflow"); - binary.resize(newOffset, 0); - binary.insert(binary.end(), trie.begin(), trie.end()); - std::fill(binary.begin() + oldOffset, binary.begin() + oldOffset + oldSize, 0); - segment.FileSize = binary.size() - segment.FileOffset; - segment.VmSize = std::max(segment.VmSize, (segment.FileSize + kPageSize - 1) & ~(kPageSize - 1)); - Write(binary, *linkedit, segment); - Write(binary, location.OffsetField, static_cast(newOffset)); - Write(binary, location.SizeField, static_cast(trie.size())); + WriteLinkeditData(binary, image, {commandOffset + 8, commandOffset + 12}, data); } + } std::vector ReadMachOExports(const std::vector& binary) { @@ -464,8 +503,12 @@ std::vector ReadMachOExports(const std::vector& binar return ParseTrie(binary, Read(binary, location->OffsetField), Read(binary, location->SizeField)); } -void MachONidPatcher::PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports) const { +void MachONidPatcher::PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports, const PatchOptions& options) const { const auto image = ParseImage(binary); + if (options.executable) { + PatchChainedImports(binary, image, excludedExports, options); + return; + } const auto location = FindTrie(binary, image); if (!location) throw std::runtime_error("no export trie"); auto exports = ParseTrie(binary, Read(binary, location->OffsetField), Read(binary, location->SizeField)); @@ -486,8 +529,8 @@ void MachONidPatcher::PatchNids(std::vector& binary, const std::st if (found == nidMap.end()) throw std::runtime_error("symbol not in nid map: " + name); exports[index].Name = "_" + found->second; } - WriteTrie(binary, image, *location, TrieBuilder(exports).Build()); - PatchChainedImports(binary, ParseImage(binary)); + WriteLinkeditData(binary, image, *location, TrieBuilder(exports).Build()); + PatchChainedImports(binary, ParseImage(binary), excludedExports, options); } } diff --git a/core/libs/nid/src/NidResolver.cpp b/core/libs/nid/src/NidResolver.cpp index 200565a6a7..aefa72ee8c 100644 --- a/core/libs/nid/src/NidResolver.cpp +++ b/core/libs/nid/src/NidResolver.cpp @@ -3,21 +3,32 @@ #include #include #include +#include #include #include #include namespace Nid { +bool IsSceImport(const std::string& name) { + return name.ends_with(Internal::kNidPostfix) || (name.size() >= 3 && + std::tolower(static_cast(name[0])) == 's' && + std::tolower(static_cast(name[1])) == 'c' && + std::tolower(static_cast(name[2])) == 'e'); +} + +std::unordered_map ResolveImportNids(const std::vector& names, const std::unordered_set& excluded, const PatchOptions& options) { + const std::unordered_set unique(names.begin(), names.end()); + auto result = ResolveNids(std::vector(unique.begin(), unique.end()), "", excluded); + for (const auto& name : unique) { + if ((!options.executable || options.onlySce) && !IsSceImport(name)) result.at(name) = name; + else if (!options.executable) result.at(name) = ResolveOneName(name); + } + return result; +} + std::string ResolveOneName(const std::string& funcName) { - using namespace Internal; - - if (IsNidNoPatchCut(funcName)) - return StripNidNoPatchCut(funcName); - if (IsNidNoPatch(funcName)) - return funcName; - - return ComputeNid(StripNidPostfix(funcName), ""); + return ResolveNids({funcName}, "", {}).at(funcName); } std::unordered_map ResolveNids(const std::vector& exportedNames, const std::string& libraryName, const std::unordered_set& excludedExports) { diff --git a/core/libs/nid/src/PeNidPatcher.cpp b/core/libs/nid/src/PeNidPatcher.cpp index 58fb8ade0a..638f3bb29e 100644 --- a/core/libs/nid/src/PeNidPatcher.cpp +++ b/core/libs/nid/src/PeNidPatcher.cpp @@ -36,7 +36,7 @@ std::size_t RvaToOffset(const std::vector& pe, std::uint32_t rva, } -void PeNidPatcher::PatchNids(std::vector& pe, const std::string& libraryName, const std::unordered_set& excludedExports) const { +void PeNidPatcher::PatchNids(std::vector& pe, const std::string& libraryName, const std::unordered_set& excludedExports, const PatchOptions& options) const { using namespace Internal; if (pe.size() < 0x40) throw std::runtime_error("file too small"); @@ -54,7 +54,7 @@ void PeNidPatcher::PatchNids(std::vector& pe, const std::string& l const std::size_t sectionTableOffset = coffHeaderOffset + 20u + sizeOfOptionalHeader; const auto symbolTable = Read(pe, coffHeaderOffset + 8u); const auto symbolCount = Read(pe, coffHeaderOffset + 12u); - for (std::uint16_t i = 0; i < numberOfSections; ++i) { + for (std::uint16_t i = 0; !options.executable && i < numberOfSections; ++i) { const auto offset = sectionTableOffset + i * sizeof(PeSectionHeader); auto section = Read(pe, offset); if (pe[offset] != '/' || !symbolTable) continue; @@ -79,159 +79,162 @@ void PeNidPatcher::PatchNids(std::vector& pe, const std::string& l throw std::runtime_error("unsupported optional header magic"); } - const auto exportDir = Read(pe, dataDirectoryOffset); - if (exportDir.VirtualAddress == 0u) throw std::runtime_error("no export directory"); + if (!options.executable) { + const auto exportDir = Read(pe, dataDirectoryOffset); + if (exportDir.VirtualAddress == 0u) throw std::runtime_error("no export directory"); - const std::size_t exportDirOffset = RvaToOffset(pe, exportDir.VirtualAddress, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); - const auto exportTable = Read(pe, exportDirOffset); + const std::size_t exportDirOffset = RvaToOffset(pe, exportDir.VirtualAddress, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); + const auto exportTable = Read(pe, exportDirOffset); - if (exportTable.NumberOfNames == 0u) throw std::runtime_error("no exported names"); + if (exportTable.NumberOfNames == 0u) throw std::runtime_error("no exported names"); - const std::size_t namesArrayOffset = RvaToOffset(pe, exportTable.AddressOfNames, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); - const std::size_t ordinalsArrayOffset = RvaToOffset(pe, exportTable.AddressOfNameOrdinals, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); + const std::size_t namesArrayOffset = RvaToOffset(pe, exportTable.AddressOfNames, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); + const std::size_t ordinalsArrayOffset = RvaToOffset(pe, exportTable.AddressOfNameOrdinals, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); - std::vector nameRvas(exportTable.NumberOfNames); - std::vector names(exportTable.NumberOfNames); - for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) { - const auto nameRva = Read(pe, namesArrayOffset + i * 4u); - const std::size_t nameOffset = RvaToOffset(pe, nameRva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); - const std::string name = ReadCStr(pe, nameOffset); - if (name.empty()) throw std::runtime_error("empty exported name"); - nameRvas[i] = nameRva; - names[i] = name; - } - - const auto nidMap = ResolveNids(names, libraryName, excludedExports); - - const std::size_t edataSectionOffset = FindSectionOffsetByRva(pe, exportDir.VirtualAddress, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); - const auto edataSection = Read(pe, edataSectionOffset); - - const auto fileAlignment = Read(pe, optionalHeaderOffset + 36u); - if (fileAlignment == 0u) throw std::runtime_error("invalid file alignment"); - - std::uint32_t stringsRegionStart = std::numeric_limits::max(); - for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) { - const std::uint32_t rvaFromSection = nameRvas[i] - edataSection.VirtualAddress; - if (rvaFromSection < stringsRegionStart) stringsRegionStart = rvaFromSection; - } - if (stringsRegionStart == std::numeric_limits::max()) - throw std::runtime_error("could not determine strings region start"); - - std::uint32_t rawLimit = ((edataSection.SizeOfRawData + fileAlignment - 1u) / fileAlignment) * fileAlignment; - const auto dllName = ReadCStr(pe, RvaToOffset(pe, exportTable.Name, peHeaderOffset, numberOfSections, sizeOfOptionalHeader)); - const bool repackDllName = exportTable.Name >= edataSection.VirtualAddress && - exportTable.Name - edataSection.VirtualAddress < rawLimit; - if (repackDllName) stringsRegionStart = std::min(stringsRegionStart, exportTable.Name - edataSection.VirtualAddress); - struct Forwarder { - std::size_t EntryOffset; - std::string Target; - }; - std::vector forwarders; - std::vector directRvas; - const auto functionsArrayOffset = RvaToOffset(pe, exportTable.AddressOfFunctions, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); - for (std::uint32_t index = 0u; index < exportTable.NumberOfFunctions; ++index) { - const auto entryOffset = functionsArrayOffset + index * sizeof(std::uint32_t); - const auto rva = Read(pe, entryOffset); - if (rva < exportDir.VirtualAddress || std::uint64_t{rva} - exportDir.VirtualAddress >= exportDir.Size) { - directRvas.push_back(rva); - continue; + std::vector nameRvas(exportTable.NumberOfNames); + std::vector names(exportTable.NumberOfNames); + for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) { + const auto nameRva = Read(pe, namesArrayOffset + i * 4u); + const std::size_t nameOffset = RvaToOffset(pe, nameRva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); + const std::string name = ReadCStr(pe, nameOffset); + if (name.empty()) throw std::runtime_error("empty exported name"); + nameRvas[i] = nameRva; + names[i] = name; } - const auto target = ReadCStr(pe, RvaToOffset(pe, rva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader)); - if (target.size() + 1u > exportDir.Size - (rva - exportDir.VirtualAddress)) - throw std::runtime_error("forwarder string outside export directory"); - forwarders.push_back({entryOffset, target}); - stringsRegionStart = std::min(stringsRegionStart, rva - edataSection.VirtualAddress); - } - for (const auto rva : directRvas) { - if (rva >= edataSection.VirtualAddress && rva - edataSection.VirtualAddress >= stringsRegionStart) - rawLimit = std::min(rawLimit, rva - edataSection.VirtualAddress); - } - const auto reserveTable = [&](std::uint32_t rva, std::uint64_t size) { - if (rva < edataSection.VirtualAddress || rva - edataSection.VirtualAddress >= rawLimit) return; - const auto end = std::uint64_t{rva - edataSection.VirtualAddress} + size; - if (end > rawLimit) throw std::runtime_error("export table outside edata section"); - stringsRegionStart = std::max(stringsRegionStart, static_cast(end)); - }; - reserveTable(exportDir.VirtualAddress, sizeof(PeExportDirectory)); - reserveTable(exportTable.AddressOfFunctions, std::uint64_t{exportTable.NumberOfFunctions} * 4u); - reserveTable(exportTable.AddressOfNames, std::uint64_t{exportTable.NumberOfNames} * 4u); - reserveTable(exportTable.AddressOfNameOrdinals, std::uint64_t{exportTable.NumberOfNames} * 2u); - std::vector finalNames(exportTable.NumberOfNames); - for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) { - const auto it = nidMap.find(names[i]); - if (it == nidMap.end()) throw std::runtime_error("symbol not in nid map: " + names[i]); - finalNames[i] = it->second; - } + const auto nidMap = ResolveNids(names, libraryName, excludedExports); - std::vector strings; - std::unordered_map stringRvas; - const auto storeString = [&](const std::string& name) { - const auto existing = stringRvas.find(name); - if (existing != stringRvas.end()) return existing->second; - const auto rva = edataSection.VirtualAddress + stringsRegionStart + static_cast(strings.size()); - stringRvas.emplace(name, rva); - strings.insert(strings.end(), name.begin(), name.end()); - strings.push_back(0u); - return rva; - }; - std::vector forwarderRvas; - for (const auto& forwarder : forwarders) forwarderRvas.push_back(storeString(forwarder.Target)); - if (!forwarders.empty() && (std::uint64_t{edataSection.VirtualAddress} + stringsRegionStart < exportDir.VirtualAddress || - std::uint64_t{edataSection.VirtualAddress} + stringsRegionStart + strings.size() > std::uint64_t{exportDir.VirtualAddress} + exportDir.Size)) - throw std::runtime_error("not enough export directory space to repack forwarders"); - const auto dllNameRva = repackDllName ? storeString(dllName) : exportTable.Name; - std::vector finalNameRvas; - for (const auto& name : finalNames) finalNameRvas.push_back(storeString(name)); - const auto requiredSize = std::uint64_t{stringsRegionStart} + strings.size(); - if (requiredSize > rawLimit) - throw std::runtime_error("not enough raw space in edata section to repack export names"); - const auto directoryEnd = std::max(std::uint64_t{exportDir.VirtualAddress} + exportDir.Size, - std::uint64_t{edataSection.VirtualAddress} + requiredSize); - for (const auto rva : directRvas) { - if (rva != 0u && rva >= exportDir.VirtualAddress && std::uint64_t{rva} < directoryEnd) - throw std::runtime_error("repacked export directory would cover a direct export"); - } - const auto directorySize = directoryEnd - exportDir.VirtualAddress; - if (directorySize > std::numeric_limits::max()) throw std::runtime_error("export directory too large"); - const auto stringFileOffset = static_cast(edataSection.PointerToRawData) + stringsRegionStart; - if (stringFileOffset > pe.size() || strings.size() > pe.size() - stringFileOffset) - throw std::runtime_error("repack write out of file bounds"); - std::memcpy(pe.data() + stringFileOffset, strings.data(), strings.size()); - for (std::size_t index = 0u; index < finalNameRvas.size(); ++index) - Write(pe, namesArrayOffset + index * 4u, finalNameRvas[index]); - for (std::size_t index = 0u; index < forwarders.size(); ++index) - Write(pe, forwarders[index].EntryOffset, forwarderRvas[index]); - Write(pe, exportDirOffset + offsetof(PeExportDirectory, Name), dllNameRva); - Write(pe, dataDirectoryOffset + offsetof(PeDataDirectory, Size), static_cast(directorySize)); + const std::size_t edataSectionOffset = FindSectionOffsetByRva(pe, exportDir.VirtualAddress, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); + const auto edataSection = Read(pe, edataSectionOffset); - const std::uint32_t newVirtualSize = std::max(edataSection.VirtualSize, static_cast(requiredSize)); - auto patchedSection = edataSection; - patchedSection.VirtualSize = newVirtualSize; - Write(pe, edataSectionOffset, patchedSection); + const auto fileAlignment = Read(pe, optionalHeaderOffset + 36u); + if (fileAlignment == 0u) throw std::runtime_error("invalid file alignment"); - struct ExportName { - std::string Name; - std::uint32_t Rva; - std::uint16_t Ordinal; - }; + std::uint32_t stringsRegionStart = std::numeric_limits::max(); + for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) { + const std::uint32_t rvaFromSection = nameRvas[i] - edataSection.VirtualAddress; + if (rvaFromSection < stringsRegionStart) stringsRegionStart = rvaFromSection; + } + if (stringsRegionStart == std::numeric_limits::max()) + throw std::runtime_error("could not determine strings region start"); + + std::uint32_t rawLimit = ((edataSection.SizeOfRawData + fileAlignment - 1u) / fileAlignment) * fileAlignment; + const auto dllName = ReadCStr(pe, RvaToOffset(pe, exportTable.Name, peHeaderOffset, numberOfSections, sizeOfOptionalHeader)); + const bool repackDllName = exportTable.Name >= edataSection.VirtualAddress && + exportTable.Name - edataSection.VirtualAddress < rawLimit; + if (repackDllName) stringsRegionStart = std::min(stringsRegionStart, exportTable.Name - edataSection.VirtualAddress); + struct Forwarder { + std::size_t EntryOffset; + std::string Target; + }; + std::vector forwarders; + std::vector directRvas; + const auto functionsArrayOffset = RvaToOffset(pe, exportTable.AddressOfFunctions, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); + for (std::uint32_t index = 0u; index < exportTable.NumberOfFunctions; ++index) { + const auto entryOffset = functionsArrayOffset + index * sizeof(std::uint32_t); + const auto rva = Read(pe, entryOffset); + if (rva < exportDir.VirtualAddress || std::uint64_t{rva} - exportDir.VirtualAddress >= exportDir.Size) { + directRvas.push_back(rva); + continue; + } + const auto target = ReadCStr(pe, RvaToOffset(pe, rva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader)); + if (target.size() + 1u > exportDir.Size - (rva - exportDir.VirtualAddress)) + throw std::runtime_error("forwarder string outside export directory"); + forwarders.push_back({entryOffset, target}); + stringsRegionStart = std::min(stringsRegionStart, rva - edataSection.VirtualAddress); + } + for (const auto rva : directRvas) { + if (rva >= edataSection.VirtualAddress && rva - edataSection.VirtualAddress >= stringsRegionStart) + rawLimit = std::min(rawLimit, rva - edataSection.VirtualAddress); + } + const auto reserveTable = [&](std::uint32_t rva, std::uint64_t size) { + if (rva < edataSection.VirtualAddress || rva - edataSection.VirtualAddress >= rawLimit) return; + const auto end = std::uint64_t{rva - edataSection.VirtualAddress} + size; + if (end > rawLimit) throw std::runtime_error("export table outside edata section"); + stringsRegionStart = std::max(stringsRegionStart, static_cast(end)); + }; + reserveTable(exportDir.VirtualAddress, sizeof(PeExportDirectory)); + reserveTable(exportTable.AddressOfFunctions, std::uint64_t{exportTable.NumberOfFunctions} * 4u); + reserveTable(exportTable.AddressOfNames, std::uint64_t{exportTable.NumberOfNames} * 4u); + reserveTable(exportTable.AddressOfNameOrdinals, std::uint64_t{exportTable.NumberOfNames} * 2u); + + std::vector finalNames(exportTable.NumberOfNames); + for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) { + const auto it = nidMap.find(names[i]); + if (it == nidMap.end()) throw std::runtime_error("symbol not in nid map: " + names[i]); + finalNames[i] = it->second; + } + + std::vector strings; + std::unordered_map stringRvas; + const auto storeString = [&](const std::string& name) { + const auto existing = stringRvas.find(name); + if (existing != stringRvas.end()) return existing->second; + const auto rva = edataSection.VirtualAddress + stringsRegionStart + static_cast(strings.size()); + stringRvas.emplace(name, rva); + strings.insert(strings.end(), name.begin(), name.end()); + strings.push_back(0u); + return rva; + }; + std::vector forwarderRvas; + for (const auto& forwarder : forwarders) forwarderRvas.push_back(storeString(forwarder.Target)); + if (!forwarders.empty() && (std::uint64_t{edataSection.VirtualAddress} + stringsRegionStart < exportDir.VirtualAddress || + std::uint64_t{edataSection.VirtualAddress} + stringsRegionStart + strings.size() > std::uint64_t{exportDir.VirtualAddress} + exportDir.Size)) + throw std::runtime_error("not enough export directory space to repack forwarders"); + const auto dllNameRva = repackDllName ? storeString(dllName) : exportTable.Name; + std::vector finalNameRvas; + for (const auto& name : finalNames) finalNameRvas.push_back(storeString(name)); + const auto requiredSize = std::uint64_t{stringsRegionStart} + strings.size(); + if (requiredSize > rawLimit) + throw std::runtime_error("not enough raw space in edata section to repack export names"); + const auto directoryEnd = std::max(std::uint64_t{exportDir.VirtualAddress} + exportDir.Size, + std::uint64_t{edataSection.VirtualAddress} + requiredSize); + for (const auto rva : directRvas) { + if (rva != 0u && rva >= exportDir.VirtualAddress && std::uint64_t{rva} < directoryEnd) + throw std::runtime_error("repacked export directory would cover a direct export"); + } + const auto directorySize = directoryEnd - exportDir.VirtualAddress; + if (directorySize > std::numeric_limits::max()) throw std::runtime_error("export directory too large"); + const auto stringFileOffset = static_cast(edataSection.PointerToRawData) + stringsRegionStart; + if (stringFileOffset > pe.size() || strings.size() > pe.size() - stringFileOffset) + throw std::runtime_error("repack write out of file bounds"); + std::memcpy(pe.data() + stringFileOffset, strings.data(), strings.size()); + for (std::size_t index = 0u; index < finalNameRvas.size(); ++index) + Write(pe, namesArrayOffset + index * 4u, finalNameRvas[index]); + for (std::size_t index = 0u; index < forwarders.size(); ++index) + Write(pe, forwarders[index].EntryOffset, forwarderRvas[index]); + Write(pe, exportDirOffset + offsetof(PeExportDirectory, Name), dllNameRva); + Write(pe, dataDirectoryOffset + offsetof(PeDataDirectory, Size), static_cast(directorySize)); + + const std::uint32_t newVirtualSize = std::max(edataSection.VirtualSize, static_cast(requiredSize)); + auto patchedSection = edataSection; + patchedSection.VirtualSize = newVirtualSize; + Write(pe, edataSectionOffset, patchedSection); + + struct ExportName { + std::string Name; + std::uint32_t Rva; + std::uint16_t Ordinal; + }; + + std::vector exportNames; + exportNames.reserve(exportTable.NumberOfNames); + for (std::size_t i = 0; i < exportTable.NumberOfNames; ++i) { + const auto nameRva = Read(pe, namesArrayOffset + i * sizeof(std::uint32_t)); + const auto ordinal = Read(pe, ordinalsArrayOffset + i * sizeof(std::uint16_t)); + if (ordinal >= exportTable.NumberOfFunctions) + throw std::runtime_error("export name ordinal out of bounds"); + const auto nameOffset = RvaToOffset(pe, nameRva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); + exportNames.push_back({ReadCStr(pe, nameOffset), nameRva, ordinal}); + } + std::sort(exportNames.begin(), exportNames.end(), [](const ExportName& left, const ExportName& right) { return left.Name < right.Name; }); + for (std::size_t i = 0; i < exportNames.size(); ++i) { + if (i != 0 && exportNames[i - 1].Name == exportNames[i].Name) + throw std::runtime_error("duplicate patched export name: " + exportNames[i].Name); + Write(pe, namesArrayOffset + i * sizeof(std::uint32_t), exportNames[i].Rva); + Write(pe, ordinalsArrayOffset + i * sizeof(std::uint16_t), exportNames[i].Ordinal); + } - std::vector exportNames; - exportNames.reserve(exportTable.NumberOfNames); - for (std::size_t i = 0; i < exportTable.NumberOfNames; ++i) { - const auto nameRva = Read(pe, namesArrayOffset + i * sizeof(std::uint32_t)); - const auto ordinal = Read(pe, ordinalsArrayOffset + i * sizeof(std::uint16_t)); - if (ordinal >= exportTable.NumberOfFunctions) - throw std::runtime_error("export name ordinal out of bounds"); - const auto nameOffset = RvaToOffset(pe, nameRva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); - exportNames.push_back({ReadCStr(pe, nameOffset), nameRva, ordinal}); - } - std::sort(exportNames.begin(), exportNames.end(), [](const ExportName& left, const ExportName& right) { return left.Name < right.Name; }); - for (std::size_t i = 0; i < exportNames.size(); ++i) { - if (i != 0 && exportNames[i - 1].Name == exportNames[i].Name) - throw std::runtime_error("duplicate patched export name: " + exportNames[i].Name); - Write(pe, namesArrayOffset + i * sizeof(std::uint32_t), exportNames[i].Rva); - Write(pe, ordinalsArrayOffset + i * sizeof(std::uint16_t), exportNames[i].Ordinal); } constexpr std::size_t kImportDirIndex = 1u; @@ -250,6 +253,20 @@ void PeNidPatcher::PatchNids(std::vector& pe, const std::string& l std::uint32_t FirstThunk; }; + if (options.executable) { + for (const auto index : {4u, 11u, 13u}) { + const auto directory = Read(pe, dataDirectoryOffset + index * sizeof(PeDataDirectory)); + if (directory.VirtualAddress || directory.Size) throw std::runtime_error("signed, bound or delay-loaded PE imports are not supported in --exec mode"); + } + } + struct ImportUse { + std::size_t NameOffset; + std::size_t LookupOffset; + std::size_t AddressOffset; + std::string Name; + }; + std::vector imports; + std::vector importNames; std::size_t importDescOffset = RvaToOffset(pe, importDir.VirtualAddress, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); while (true) { @@ -286,19 +303,9 @@ void PeNidPatcher::PatchNids(std::vector& pe, const std::string& l const std::string funcName = ReadCStr(pe, hintNameOffset + 2u); if (!funcName.empty()) { - const bool hasNidPostfix = funcName.size() >= kNidPostfixLen && - funcName.compare(funcName.size() - kNidPostfixLen, kNidPostfixLen, kNidPostfix) == 0; - const bool hasScePrefix = funcName.size() >= 3u && - std::tolower(static_cast(funcName[0])) == 's' && - std::tolower(static_cast(funcName[1])) == 'c' && - std::tolower(static_cast(funcName[2])) == 'e'; - if (hasNidPostfix || hasScePrefix) { - const std::string nid = ResolveOneName(funcName); - if (nid.size() > funcName.size()) - throw std::runtime_error("import NID longer than original name: " + funcName); - std::memcpy(pe.data() + hintNameOffset + 2u, nid.data(), nid.size()); - pe[hintNameOffset + 2u + nid.size()] = 0u; - } + const auto addressOffset = RvaToOffset(pe, desc.FirstThunk, peHeaderOffset, numberOfSections, sizeOfOptionalHeader) + thunkOffset - RvaToOffset(pe, thunkRva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); + imports.push_back({hintNameOffset + 2u, thunkOffset, addressOffset, funcName}); + importNames.push_back(funcName); } } @@ -307,6 +314,72 @@ void PeNidPatcher::PatchNids(std::vector& pe, const std::string& l importDescOffset += sizeof(PeImportDescriptor); } + const auto importMap = ResolveImportNids(importNames, excludedExports, options); + if (!options.executable) { + for (const auto& item : imports) { + const auto& name = importMap.at(item.Name); + if (name.size() > item.Name.size()) throw std::runtime_error("import NID longer than original name: " + item.Name); + std::memcpy(pe.data() + item.NameOffset, name.c_str(), name.size() + 1); + } + return; + } + std::vector strings; + std::unordered_map stringOffsets; + for (const auto& item : imports) { + const auto& name = importMap.at(item.Name); + if (name == item.Name || stringOffsets.contains(name)) continue; + if (strings.size() & 1u) strings.push_back(0); + if (strings.size() > 0xffffffffu - name.size() - 3u) throw std::runtime_error("PE import string table overflow"); + stringOffsets.emplace(name, static_cast(strings.size())); + strings.insert(strings.end(), {0, 0}); + strings.insert(strings.end(), name.begin(), name.end()); + strings.push_back(0); + } + if (strings.empty()) return; + const auto fileAlignment = Read(pe, optionalHeaderOffset + 36u); + const auto sectionAlignment = Read(pe, optionalHeaderOffset + 32u); + const auto align = [](std::uint64_t value, std::uint32_t alignment) { + if (!alignment || (alignment & (alignment - 1u))) throw std::runtime_error("invalid PE alignment"); + const auto result = (value + alignment - 1u) & ~std::uint64_t{alignment - 1u}; + if (result > 0xffffffffu) throw std::runtime_error("PE section overflow"); + return static_cast(result); + }; + const auto newHeader = sectionTableOffset + numberOfSections * sizeof(PeSectionHeader); + auto headerLimit = std::size_t{Read(pe, optionalHeaderOffset + 60u)}; + std::uint64_t virtualEnd = headerLimit; + for (std::uint16_t index = 0; index < numberOfSections; ++index) { + const auto section = Read(pe, sectionTableOffset + index * sizeof(PeSectionHeader)); + if (section.PointerToRawData) headerLimit = std::min(headerLimit, std::size_t{section.PointerToRawData}); + virtualEnd = std::max(virtualEnd, std::uint64_t{section.VirtualAddress} + std::max(section.VirtualSize, section.SizeOfRawData)); + } + if (numberOfSections == 0xffffu || newHeader + sizeof(PeSectionHeader) > headerLimit || headerLimit > pe.size()) throw std::runtime_error("no room for PE import section header"); + PeSectionHeader section{}; + const char sectionName[] = ".nidimp"; + std::memcpy(section.Name, sectionName, sizeof(sectionName)); + section.VirtualSize = static_cast(strings.size()); + section.VirtualAddress = align(virtualEnd, sectionAlignment); + section.PointerToRawData = align(pe.size(), fileAlignment); + section.SizeOfRawData = align(strings.size(), fileAlignment); + section.Characteristics = 0x40000040u; + const auto imageSize = align(std::uint64_t{section.VirtualAddress} + section.VirtualSize, sectionAlignment); + const auto rawEnd = std::uint64_t{section.PointerToRawData} + section.SizeOfRawData; + if (rawEnd > 0xffffffffu) throw std::runtime_error("PE file size overflow"); + pe.resize(static_cast(rawEnd), 0); + std::copy(strings.begin(), strings.end(), pe.begin() + section.PointerToRawData); + Write(pe, newHeader, section); + Write(pe, coffHeaderOffset + 2u, static_cast(numberOfSections + 1u)); + Write(pe, optionalHeaderOffset + 56u, imageSize); + Write(pe, optionalHeaderOffset + 64u, std::uint32_t{0}); + for (const auto& item : imports) { + const auto& name = importMap.at(item.Name); + if (name == item.Name) continue; + const auto rva = section.VirtualAddress + stringOffsets.at(name); + for (const auto offset : {item.LookupOffset, item.AddressOffset}) { + if (magic == 0x20bu) Write(pe, offset, std::uint64_t{rva}); + else Write(pe, offset, rva); + } + } + } } diff --git a/core/libs/nid/src/main.cpp b/core/libs/nid/src/main.cpp index 722c765e7c..776480fca6 100644 --- a/core/libs/nid/src/main.cpp +++ b/core/libs/nid/src/main.cpp @@ -28,6 +28,7 @@ void _writeFile(const std::string& path, const std::vector& data) int Run(int argc, char* argv[]) { if (argc < 3) { std::cerr << "Usage: nid_patcher [--preserve-exports | --preserve-export-list ] [file2 ...]\n" + " nid_patcher --exec [--only-sce] [--preserve-exports | --preserve-export-list ] [file2 ...]\n" " nid_patcher --write-export-list \n"; return 1; } @@ -46,14 +47,20 @@ int Run(int argc, char* argv[]) { return 0; } - const std::string libraryName = argv[1]; + Nid::PatchOptions options; + options.executable = std::string(argv[1]) == "--exec"; + const std::string libraryName = options.executable ? "" : argv[1]; std::unordered_set excludedExports; std::vector paths; try { + if (!options.executable && libraryName.starts_with("--")) throw std::runtime_error("unknown option: " + libraryName); bool hasReference = false; for (int i = 2; i < argc; ++i) { const std::string argument = argv[i]; - if (argument == "--preserve-exports" || argument == "--preserve-export-list") { + if (argument == "--only-sce") { + if (!options.executable || options.onlySce) throw std::runtime_error("--only-sce requires --exec and may appear once"); + options.onlySce = true; + } else if (argument == "--preserve-exports" || argument == "--preserve-export-list") { if (hasReference || i + 1 == argc) throw std::runtime_error(argument + " requires exactly one reference"); hasReference = true; excludedExports = argument == "--preserve-exports" ? Nid::ReadExportExclusions(argv[++i]) : Nid::ReadExportList(argv[++i]); @@ -72,7 +79,7 @@ int Run(int argc, char* argv[]) { try { auto binary = _readFile(path); const auto patcher = Nid::MakePatcher(binary); - patcher->PatchNids(binary, libraryName, excludedExports); + patcher->PatchNids(binary, libraryName, excludedExports, options); _writeFile(path, binary); std::cout << "OK: " << path << "\n"; } catch (const std::exception& e) { diff --git a/core/libs/nid/tests/test_elf_duplicates.py b/core/libs/nid/tests/test_elf_duplicates.py index 504cf33bdc..25f9853da1 100644 --- a/core/libs/nid/tests/test_elf_duplicates.py +++ b/core/libs/nid/tests/test_elf_duplicates.py @@ -4,7 +4,7 @@ import sys import unittest from pathlib import Path -from test_symbol_versions import build_image, patch, read_dynsym, IMPORTS, NID_NAME, SHN_UNDEF, STT_FUNC +from test_symbol_versions import build_image, patch, read_dynsym, IMPORTS, NID_NAME, SHN_UNDEF, STT_FUNC, PATCHER def nid(name): @@ -21,6 +21,7 @@ def image(names, addresses=None): return bytes(build_image(entries, False)) +@unittest.skipUnless(PATCHER is not None and PATCHER.is_file(), "nid_patcher executable required") class ElfDuplicateTests(unittest.TestCase): def test_exports_patched_to_one_name_are_rejected(self): cases = ( @@ -65,5 +66,4 @@ class ElfDuplicateTests(unittest.TestCase): if __name__ == "__main__": - PATCHER = Path(sys.argv.pop(1)).resolve() unittest.main() diff --git a/core/libs/nid/tests/test_elf_layout.py b/core/libs/nid/tests/test_elf_layout.py index 703ac46db3..d4060852f6 100644 --- a/core/libs/nid/tests/test_elf_layout.py +++ b/core/libs/nid/tests/test_elf_layout.py @@ -3,9 +3,10 @@ import sys import unittest from pathlib import Path -from test_symbol_versions import build_image, symbols, patch, SHOFF +from test_symbol_versions import build_image, symbols, patch, SHOFF, PATCHER +@unittest.skipUnless(PATCHER is not None and PATCHER.is_file(), "nid_patcher executable required") class ElfLayoutTests(unittest.TestCase): def test_declared_layout_must_match_supported_tables(self): changes = ((5, "B", 2), (0x3a, "H", 32), @@ -44,5 +45,4 @@ class ElfLayoutTests(unittest.TestCase): if __name__ == "__main__": - PATCHER = Path(sys.argv.pop(1)).resolve() unittest.main() diff --git a/core/libs/nid/tests/test_gnu_hash.py b/core/libs/nid/tests/test_gnu_hash.py index 2e46a70f32..c1ddde43d4 100644 --- a/core/libs/nid/tests/test_gnu_hash.py +++ b/core/libs/nid/tests/test_gnu_hash.py @@ -3,7 +3,7 @@ import sys import unittest from pathlib import Path -from test_symbol_versions import build_image, symbols, patch, HASH_OFFSET, SHOFF, read_dynsym, gnu_hash, sections +from test_symbol_versions import build_image, symbols, patch, HASH_OFFSET, SHOFF, read_dynsym, gnu_hash, sections, PATCHER def sysv_hash(name): @@ -31,6 +31,7 @@ def sysv_lookup(image, name): return None +@unittest.skipUnless(PATCHER is not None and PATCHER.is_file(), "nid_patcher executable required") class GnuHashTests(unittest.TestCase): def image(self, buckets=4, bloom=1, shift=6, symoffset=3): image = build_image(symbols(False), False) @@ -95,6 +96,7 @@ class GnuHashTests(unittest.TestCase): self.assertEqual(struct.unpack_from(" 1: + patcher = Path(sys.argv[1]).resolve() + else: + patcher = PATCHER or find_patcher() + if patcher is None: + print("nid_patcher executable required") + sys.exit(0) with tempfile.TemporaryDirectory(prefix="anyps5-macho-nid-a\u00e7\u00e3o-\u65e5\u672c-") as directory: check_shrinking(patcher, directory) check_growing(patcher, directory) diff --git a/core/libs/nid/tests/test_native_paths.py b/core/libs/nid/tests/test_native_paths.py index ccfd9e6b8c..4b5c12193a 100644 --- a/core/libs/nid/tests/test_native_paths.py +++ b/core/libs/nid/tests/test_native_paths.py @@ -5,16 +5,14 @@ import tempfile import unittest from pathlib import Path -from test_macho_nids import build_dylib, exports_of, nid +from test_macho_nids import build_dylib, exports_of, nid, PATCHER try: import resource except ImportError: resource = None -PATCHER = Path(sys.argv.pop(1)).resolve() - - +@unittest.skipUnless(PATCHER is not None and PATCHER.is_file(), "nid_patcher executable required") class NativePathTests(unittest.TestCase): def invoke(self, *arguments): return subprocess.run([str(PATCHER), "libtest", *map(str, arguments)], diff --git a/core/libs/nid/tests/test_pe_references.py b/core/libs/nid/tests/test_pe_references.py index 2e6eed7f40..2992a55093 100644 --- a/core/libs/nid/tests/test_pe_references.py +++ b/core/libs/nid/tests/test_pe_references.py @@ -5,7 +5,7 @@ import tempfile import unittest from pathlib import Path -from test_macho_nids import build_dylib, exports_of, nid +from test_macho_nids import build_dylib, exports_of, nid, PATCHER def reference(*, headers=False, address=0x2000, ordinal=0): @@ -100,6 +100,7 @@ def forwarding_exports(image, bits): return string(table[4]), names, targets, addresses, (export_rva, export_size) +@unittest.skipUnless(PATCHER is not None and PATCHER.is_file(), "nid_patcher executable required") class ReferenceTests(unittest.TestCase): def patch_with_reference(self, data): with tempfile.TemporaryDirectory() as tmp: @@ -135,6 +136,7 @@ class ReferenceTests(unittest.TestCase): self.assertEqual(image, original) +@unittest.skipUnless(PATCHER is not None and PATCHER.is_file(), "nid_patcher executable required") class ForwardingExportTests(unittest.TestCase): def patch(self, image): with tempfile.TemporaryDirectory() as temporary: @@ -241,6 +243,203 @@ class ForwardingExportTests(unittest.TestCase): self.assertEqual(after, image) +class ExecutableImportTests(unittest.TestCase): + names = ["foo", "foo_nid_postfix", "z", "sceExample", "helper_nid_no_patch", "plain_nid_no_patch_cut", "SDL_Init", "scePreserved", "hostFunctionLongName", "alias_nid_disambig12_nid_postfix"] + + def pe(self, bits=64): + image = bytearray(2048) + image[:2] = b"MZ" + struct.pack_into(" 1: + patcher = Path(sys.argv[1]).resolve() + else: + patcher = PATCHER or find_patcher() + if patcher is None: + print("nid_patcher executable required") + sys.exit(0) test_versioned_library_is_rejected(patcher) test_unversioned_library_is_still_patched(patcher) print("NID patcher symbol version tests passed") diff --git a/core/libs/nid/tests/test_verneed.py b/core/libs/nid/tests/test_verneed.py index 58ebc02c6b..79dd7a7789 100644 --- a/core/libs/nid/tests/test_verneed.py +++ b/core/libs/nid/tests/test_verneed.py @@ -3,7 +3,7 @@ import sys import unittest from pathlib import Path -from test_symbol_versions import build_image, symbols, patch, SHOFF, DYNSTR_OFFSET, sections +from test_symbol_versions import build_image, symbols, patch, SHOFF, DYNSTR_OFFSET, sections, PATCHER VERNEED = 0x580 @@ -22,6 +22,7 @@ def image(*, auxiliary=16, count=1, size=32, version=1, auxnext=0, nextrecord=0) return data +@unittest.skipUnless(PATCHER is not None and PATCHER.is_file(), "nid_patcher executable required") class VersionRequirementsTests(unittest.TestCase): def test_auxiliary_record_cannot_leave_declared_section(self): self.reject(image(auxiliary=32)) @@ -72,5 +73,4 @@ class VersionRequirementsTests(unittest.TestCase): if __name__ == "__main__": - PATCHER = Path(sys.argv.pop(1)).resolve() unittest.main() diff --git a/core/libs/prx/libSceAgc/Acb/include/Sync.hpp b/core/libs/prx/libSceAgc/Acb/include/Sync.hpp index fb7a826139..c5ecc1c87d 100644 --- a/core/libs/prx/libSceAgc/Acb/include/Sync.hpp +++ b/core/libs/prx/libSceAgc/Acb/include/Sync.hpp @@ -9,7 +9,7 @@ extern "C" { uint32_t* APS5_VABI sceAgcAcbCondExec(CommandBuffer* buf, const volatile uint32_t* address, uint32_t numDwords); uint32_t APS5_VABI sceAgcAcbCondExecGetSize(void); std::uint32_t* APS5_VABI sceAgcAcbWaitRegMem(CommandBuffer* buf, std::uint8_t size, std::uint8_t compareFunction, std::uint8_t cachePolicy, const volatile void* address, std::uint64_t reference, std::uint64_t mask, std::uint32_t pollCycles); -std::uint64_t APS5_VABI sceAgcAcbWaitOnAddressGetSize(std::uint8_t size); +std::uint64_t APS5_VABI sceAgcAcbWaitOnAddressGetSize(std::uint32_t size); std::uint32_t* APS5_VABI sceAgcAcbEventWrite(CommandBuffer* buf, std::uint8_t eventType, const volatile void* address); std::uint64_t APS5_VABI sceAgcAcbEventWriteGetSize(std::uint8_t eventType); std::uint32_t APS5_VABI sceAgcAcbQueueEndOfShaderActionGetSize(); diff --git a/core/libs/prx/libSceAgc/Acb/src/Sync.cpp b/core/libs/prx/libSceAgc/Acb/src/Sync.cpp index cb4ff7f5a6..cc5b7eb20c 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Sync.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Sync.cpp @@ -25,8 +25,8 @@ std::uint32_t* APS5_VABI sceAgcAcbWaitRegMem(CommandBuffer* buf, std::uint8_t si return Agc::Command::WriteWait(buf, size, compareFunction, 0, cachePolicy, address, reference, mask, pollCycles, __func__); } -std::uint64_t APS5_VABI sceAgcAcbWaitOnAddressGetSize(std::uint8_t size) { - return size == 0 ? 56 : 64; +std::uint64_t APS5_VABI sceAgcAcbWaitOnAddressGetSize(std::uint32_t size) { + return size == 0 ? 56 : size == 1 ? 64 : 0; } std::uint32_t* APS5_VABI sceAgcAcbEventWrite(CommandBuffer* buf, std::uint8_t eventType, const volatile void* address) { diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index 9c26307068..45c2e3391a 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -293,6 +293,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_wait_on_address_size_tests EXCLUDE_FROM_ALL tests/WaitOnAddressSize.cpp) +target_include_directories(agc_wait_on_address_size_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_wait_on_address_size_tests PRIVATE libSceAgc libc) +configure_windows_unwind(agc_wait_on_address_size_tests) +if(BUILD_TESTING) + set_target_properties(agc_wait_on_address_size_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME agc_wait_on_address_size COMMAND agc_wait_on_address_size_tests) + if(WIN32) + set_tests_properties(agc_wait_on_address_size PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_interpolant_mapping_tests EXCLUDE_FROM_ALL tests/InterpolantMapping.cpp Shader/src/InterpolantMapping.cpp diff --git a/core/libs/prx/libSceAgc/Command/src/Packet.cpp b/core/libs/prx/libSceAgc/Command/src/Packet.cpp index be43fb7028..13036e5d12 100644 --- a/core/libs/prx/libSceAgc/Command/src/Packet.cpp +++ b/core/libs/prx/libSceAgc/Command/src/Packet.cpp @@ -115,17 +115,23 @@ std::uint32_t* WriteRegisters(CommandBuffer* buffer, std::uint32_t opcode, const Require(buffer != nullptr, function, "null command buffer"); return buffer->cursor_up; } - CheckAddress(reinterpret_cast(registers), 4, function); + CheckAddress(reinterpret_cast(registers), 1, function); + const auto* bytes = reinterpret_cast(const_cast(registers)); + const auto entryAt = [&](std::uint32_t index) { + ShaderRegister entry; + std::memcpy(&entry, bytes + static_cast(index) * sizeof(ShaderRegister), sizeof(entry)); + return entry; + }; std::vector snapshot; if (snapshotAll) { snapshot.reserve(count); for (std::uint32_t i = 0; i < count; ++i) { - snapshot.push_back({registers[i].offset, registers[i].value}); + snapshot.push_back(entryAt(i)); CheckBits(snapshot.back().offset, 0xffffu, function); } } - const auto offsetAt = [&](std::uint32_t index) { return snapshotAll ? snapshot[index].offset : registers[index].offset; }; - const auto valueAt = [&](std::uint32_t index) { return snapshotAll ? snapshot[index].value : registers[index].value; }; + const auto offsetAt = [&](std::uint32_t index) { return snapshotAll ? snapshot[index].offset : entryAt(index).offset; }; + const auto valueAt = [&](std::uint32_t index) { return snapshotAll ? snapshot[index].value : entryAt(index).value; }; std::uint32_t* first = nullptr; std::uint32_t index = 0; while (index < count) { diff --git a/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp b/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp index d3fb4b534b..ceea8db4ab 100644 --- a/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp +++ b/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp @@ -26,7 +26,7 @@ std::uint32_t* APS5_VABI sceAgcDcbWaitRegMem(CommandBuffer* buf, std::uint8_t si } uint32_t APS5_VABI sceAgcDcbWaitOnAddressGetSize(uint32_t size) { - return size == 0 ? 56 : 64; + return size == 0 ? 56 : size == 1 ? 64 : 0; } std::uint32_t* APS5_VABI sceAgcDcbEventWrite(CommandBuffer* buf, std::uint8_t eventType, const volatile void* address) { diff --git a/core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp b/core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp index ad1eddc97a..c0bff226ea 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp @@ -13,7 +13,9 @@ uint32_t* APS5_VABI sceAgcDcbContextStateOp(CommandBuffer* buf, uint32_t operati } uint64_t APS5_VABI sceAgcDcbContextStateOpGetSize(uint32_t operation) { - Agc::Command::Require(operation <= 3, __func__, "invalid context state operation"); + if (operation > 3) { + return 0; + } if (operation == 0) { return 5 * sizeof(std::uint32_t); } diff --git a/core/libs/prx/libSceAgc/tests/ContextStateSize.cpp b/core/libs/prx/libSceAgc/tests/ContextStateSize.cpp index 6d2e583e47..1e40ae3b66 100644 --- a/core/libs/prx/libSceAgc/tests/ContextStateSize.cpp +++ b/core/libs/prx/libSceAgc/tests/ContextStateSize.cpp @@ -47,9 +47,13 @@ void testSizes() { } } -void testRejections() { - expectFailure([] { sceAgcDcbContextStateOpGetSize(4); }); - expectFailure([] { sceAgcDcbContextStateOpGetSize(0xffffffffu); }); +void testInvalidOperations() { + for (const std::uint32_t operation : {4u, 15u, 16u, 0xffu, 0x100u, 0x103u, 0xffffffffu}) { + check(sceAgcDcbContextStateOpGetSize(operation) == 0, "an invalid context state operation did not return size 0"); + } + std::array words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + expectFailure([&] { sceAgcDcbContextStateOp_0100(&buffer, 4); }); } } @@ -57,7 +61,7 @@ void testRejections() { int main() { try { testSizes(); - testRejections(); + testInvalidOperations(); LibcRunShutdown_nid_postfix(); std::puts("AGC context state size tests passed"); return 0; diff --git a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp index 985ed914f6..4c52d26b8a 100644 --- a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp +++ b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include namespace { @@ -99,6 +100,26 @@ void testRangeFromAnyByte() { } } +void testListFromAnyByte() { + const std::array writers{sceAgcCbSetShRegistersDirect, sceAgcCbSetUcRegistersDirect}; + const std::array opcodes{0x7600u, 0x7900u}; + const std::array registers{{{0x10u, 0x11223344u}, {0x11u, 0x55667788u}, {0x30u, 0x99aabbccu}}}; + for (std::size_t i = 0; i < writers.size(); ++i) { + for (const std::size_t offset : {1u, 2u, 3u, 5u}) { + alignas(16) std::array bytes{}; + std::memcpy(bytes.data() + offset, registers.data(), sizeof(registers)); + Storage storage; + storage.words.fill(0xabcdef01u); + const auto* list = reinterpret_cast(bytes.data() + offset); + auto* packet = writers[i](&storage.buffer, list, static_cast(registers.size())); + const std::array expected{0xc0020000u | opcodes[i], 0x10u, 0x11223344u, 0x55667788u, 0xc0010000u | opcodes[i], 0x30u, 0x99aabbccu}; + check(packet == storage.words.data() && storage.buffer.cursor_up == packet + expected.size(), "unaligned register list allocation mismatch"); + check(std::equal(expected.begin(), expected.end(), packet), "unaligned register list was not read byte for byte"); + check(packet[expected.size()] == 0xabcdef01u, "unaligned register list overwrote the following word"); + } + } +} + void testIndirect() { const std::array writers{sceAgcDcbSetCxRegistersIndirect, sceAgcDcbSetShRegistersIndirect, sceAgcDcbSetUcRegistersIndirect}; const std::array sizes{sceAgcDcbSetCxRegistersIndirectGetSize, sceAgcDcbSetShRegistersIndirectGetSize, sceAgcDcbSetUcRegistersIndirectGetSize}; @@ -213,6 +234,7 @@ int main() { testDirect(); testConfig(); testRangeFromAnyByte(); + testListFromAnyByte(); testIndirect(); testIndirectPlaceholder(); testDirectList(); diff --git a/core/libs/prx/libSceAgc/tests/WaitOnAddressSize.cpp b/core/libs/prx/libSceAgc/tests/WaitOnAddressSize.cpp new file mode 100644 index 0000000000..84fe009ef6 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/WaitOnAddressSize.cpp @@ -0,0 +1,41 @@ +#include "prx/libSceAgc/Acb/include/Sync.hpp" +#include "prx/libSceAgc/DcbFlow/include/Sync.hpp" +#include "prx/libSceAgc/Command/include/Packet.hpp" + +#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}; + + std::uint64_t written() const { return static_cast(buffer.cursor_up - words.data()) * sizeof(std::uint32_t); } +}; + +int fail(const char* message) { + std::fprintf(stderr, "%s\n", message); + return 1; +} + +} + +int main() { + alignas(8) std::uint64_t label = 0; + for (const std::uint32_t size : {0u, 1u}) { + const std::uint64_t expected = size == 0 ? 56 : 64; + if (sceAgcAcbWaitOnAddressGetSize(size) != expected || sceAgcDcbWaitOnAddressGetSize(size) != expected) return fail("wait size mismatch"); + Storage acb; + Storage dcb; + sceAgcAcbWaitRegMem(&acb.buffer, static_cast(size), 3, 0, &label, 1, ~0ull, 0x40); + sceAgcDcbWaitRegMem(&dcb.buffer, static_cast(size), 3, 0, 0, &label, 1, ~0ull, 0x40); + if (acb.written() != expected || dcb.written() != expected) return fail("wait size differs from the packets the wait builders write"); + } + for (const std::uint32_t size : {2u, 3u, 4u, 8u, 0xffu, 0x100u, 0x101u, 0xffffffffu}) { + if (sceAgcAcbWaitOnAddressGetSize(size) != 0 || sceAgcDcbWaitOnAddressGetSize(size) != 0) return fail("an invalid wait size did not return 0"); + } + std::puts("AGC wait size tests passed"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 613780e647..29669383a9 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -327,6 +327,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineCache.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineJournal.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Resources.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DeferredDestruction.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PlaceholderImages.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Recorder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ParallelCompare.cpp @@ -805,7 +806,9 @@ if(BUILD_TESTING) set_tests_properties(agc_import_alias_shared PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 20) add_test(NAME agc_compared_shared_generations COMMAND agc_write_tracking_tests --compared-shared-generations) set_tests_properties(agc_compared_shared_generations PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 20) - set_tests_properties(agc_write_tracking agc_write_tracking_metadata_commit agc_resident_direct_remap agc_rewatched_direct_shared agc_import_alias_shared agc_compared_shared_generations PROPERTIES ENVIRONMENT_MODIFICATION + 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_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() @@ -860,6 +863,15 @@ if(BUILD_TESTING) add_test(NAME agc_fragment_barycentric COMMAND agc_fragment_barycentric_tests) endif() +add_test_executable(agc_tessellation_pipeline_cache_tests EXCLUDE_FROM_ALL tests/TessellationPipelineCache.cpp) +target_include_directories(agc_tessellation_pipeline_cache_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +set_target_properties(agc_tessellation_pipeline_cache_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_tessellation_pipeline_cache_tests) +if(BUILD_TESTING) + set_target_properties(agc_tessellation_pipeline_cache_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_tessellation_pipeline_cache COMMAND agc_tessellation_pipeline_cache_tests) +endif() + add_test_executable(agc_float32_flush_control_tests EXCLUDE_FROM_ALL tests/Float32FlushControl.cpp) target_include_directories(agc_float32_flush_control_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) set_target_properties(agc_float32_flush_control_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -1268,6 +1280,20 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_deferred_destruction_tests EXCLUDE_FROM_ALL tests/DeferredDestruction.cpp) +target_include_directories(agc_deferred_destruction_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +target_link_libraries(agc_deferred_destruction_tests PRIVATE libSceAgcDriver libc) +set_target_properties(agc_deferred_destruction_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_deferred_destruction_tests) +if(BUILD_TESTING) + set_target_properties(agc_deferred_destruction_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_deferred_destruction COMMAND agc_deferred_destruction_tests) + if(WIN32) + set_tests_properties(agc_deferred_destruction PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_device_memory_fallback_tests EXCLUDE_FROM_ALL tests/DeviceMemoryFallback.cpp) target_include_directories(agc_device_memory_fallback_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_device_memory_fallback_tests PRIVATE libSceAgcDriver libc) @@ -1561,8 +1587,12 @@ if(BUILD_TESTING) set_tests_properties(agc_pipeline_library_off PROPERTIES ENVIRONMENT "APS5_NO_GPL=1") add_test(NAME agc_pipeline_library_fast_only COMMAND agc_driver_recorder_tests --pipeline-library-only) set_tests_properties(agc_pipeline_library_fast_only PROPERTIES ENVIRONMENT "APS5_NO_PIPELINE_LTO=1" SKIP_RETURN_CODE 77) + add_test(NAME agc_pipeline_library_lto_info COMMAND agc_driver_recorder_tests --pipeline-library-lto-info-only) + add_test(NAME agc_pipeline_library_lto_info_fast_only COMMAND agc_driver_recorder_tests --pipeline-library-lto-info-only) + set_tests_properties(agc_pipeline_library_lto_info agc_pipeline_library_lto_info_fast_only PROPERTIES SKIP_RETURN_CODE 77) + set_tests_properties(agc_pipeline_library_lto_info_fast_only PROPERTIES ENVIRONMENT "APS5_NO_PIPELINE_LTO=1") if(WIN32) - set_tests_properties(agc_pipeline_library agc_pipeline_library_off agc_pipeline_library_fast_only PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_pipeline_library agc_pipeline_library_off agc_pipeline_library_fast_only agc_pipeline_library_lto_info agc_pipeline_library_lto_info_fast_only PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() set_tests_properties(agc_pipeline_cache agc_pipeline_cache_file PROPERTIES SKIP_RETURN_CODE 77) @@ -1572,6 +1602,8 @@ if(BUILD_TESTING) 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) + set_tests_properties(agc_import_settle_initial_dcc PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) add_test(NAME agc_import_watch_other_family COMMAND agc_driver_recorder_tests --import-watch-other-family-only) set_tests_properties(agc_import_watch_other_family PROPERTIES SKIP_RETURN_CODE 77) @@ -1589,7 +1621,7 @@ if(BUILD_TESTING) add_test(NAME agc_import_settle_retire COMMAND agc_driver_recorder_tests --settle-retire-only) add_test(NAME agc_import_settle_storage_image COMMAND agc_driver_recorder_tests --settle-storage-image-only) set_tests_properties(agc_import_settle_deferred agc_import_settle_deferred_other_family agc_import_settle_first_use agc_import_settle_earlier_import agc_import_settle_retire agc_import_settle_storage_image PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) - set_tests_properties(agc_import_watch agc_import_watch_other_family agc_import_settle_deferred agc_import_settle_deferred_other_family agc_import_settle_first_use agc_import_settle_earlier_import agc_import_settle_retire agc_import_settle_storage_image PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_import_watch agc_import_watch_other_family agc_import_settle_deferred agc_import_settle_deferred_other_family agc_import_settle_first_use agc_import_settle_earlier_import agc_import_settle_retire agc_import_settle_storage_image agc_import_settle_initial_dcc PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() if(WIN32) @@ -2168,6 +2200,12 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_address_passes_targets_split PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_address_passes_draw_transitions COMMAND agc_driver_address_passes_tests --targets) + set_tests_properties(agc_driver_address_passes_draw_transitions PROPERTIES ENVIRONMENT "APS5_NO_ADDRESS_READ_PASSES=1;APS5_DRAW_TRANSITIONS=1" SKIP_RETURN_CODE 77 TIMEOUT 60) + if(WIN32) + set_tests_properties(agc_driver_address_passes_draw_transitions PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() set_target_properties(agc_driver_rcp_hardware_tests PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME agc_driver_rcp_hardware_exact COMMAND agc_driver_rcp_hardware_tests) set_tests_properties(agc_driver_rcp_hardware_exact PROPERTIES ENVIRONMENT "APS5_EXACT_RCP=1;ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 60) @@ -2212,6 +2250,7 @@ endif() target_link_libraries(agc_driver_rect_list_corner_tests PRIVATE shader_recompiler_agc_driver) target_link_libraries(agc_driver_queue_zero_late_interrupt_tests PRIVATE libkernel) +target_link_libraries(agc_driver_linear_target_completion_points_tests PRIVATE libkernel) target_link_libraries(agc_driver_storage_file_write_tests PRIVATE libkernel) @@ -2226,6 +2265,16 @@ target_include_directories(agc_driver_image_table_cache_bounds_tests PRIVATE $) if(BUILD_TESTING) + add_test(NAME agc_driver_linear_target_completion_points_sync_flip COMMAND agc_driver_linear_target_completion_points_tests --flip) + set_tests_properties(agc_driver_linear_target_completion_points_sync_flip PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SYNC_FLIP=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_linear_target_completion_points_unpipelined COMMAND agc_driver_linear_target_completion_points_tests --label --interrupt) + set_tests_properties(agc_driver_linear_target_completion_points_unpipelined PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=0" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_linear_target_completion_points_sync_flip agc_driver_linear_target_completion_points_unpipelined PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_submit_progress COMMAND agc_driver_submit_progress_tests) set_tests_properties(agc_submit_progress PROPERTIES ENVIRONMENT "APS5_SUBMIT_PROGRESS=1;ANYPS5_REQUIRE_VULKAN=1" TIMEOUT 30 SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") @@ -2287,6 +2336,9 @@ if(BUILD_TESTING) set(testName "agc_driver_msaa_dcc_clear_${samples}") add_test(NAME ${testName} COMMAND agc_driver_alpha_to_coverage_tests --dcc-fast-clear ${samples}) list(APPEND multisampledDccClearTests ${testName}) + set(testName "agc_driver_msaa_dcc_compared_import_${samples}") + 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_msaa_dcc_state ${multisampledDccClearTests} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") @@ -2337,6 +2389,7 @@ target_include_directories(agc_driver_reversed_order_blend_tests PRIVATE $) target_include_directories(agc_driver_packed_float_color_target_tests PRIVATE $) +target_include_directories(agc_driver_uint_color_target_tests PRIVATE $) target_link_libraries(agc_driver_job_manager_compute_tests PRIVATE libSceJobManager libSceAgc libkernel) @@ -2356,6 +2409,8 @@ target_include_directories(agc_driver_color565_target_tests PRIVATE $) +target_include_directories(agc_driver_depth_bounds_emulation_draw_tests PRIVATE $) + foreach(stage IN ITEMS vert frag) set(agcProvokingShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/ProvokingVertex.${stage}) set(agcProvokingShaderSpv ${agcTestShaderGeneratedRoot}/ProvokingVertex_${stage}.spv) @@ -2474,12 +2529,14 @@ if(BUILD_TESTING) 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_table_snapshot_reuse COMMAND agc_shader_memory_tests --table-snapshot-reuse) set_tests_properties(agc_shader_table_snapshot_reuse PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_table_snapshot_reuse_unmasked COMMAND agc_shader_memory_tests --table-snapshot-reuse) + set_tests_properties(agc_shader_table_snapshot_reuse_unmasked PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_MASKED_RESULT_MEMO=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_driver_image_table_vouched_lookups COMMAND agc_driver_image_table_cache_bounds_tests --vouched-lookups) set_tests_properties(agc_driver_image_table_vouched_lookups PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_WRITE_WATCH_IMPORTS=watch" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 60) add_test(NAME agc_driver_image_table_vouch_queued_draws COMMAND agc_driver_image_table_cache_bounds_tests --queued-draws) set_tests_properties(agc_driver_image_table_vouch_queued_draws PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 60) if(WIN32) - set_tests_properties(agc_shader_table_snapshot_reuse agc_driver_image_table_vouched_lookups agc_driver_image_table_vouch_queued_draws PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_shader_table_snapshot_reuse agc_shader_table_snapshot_reuse_unmasked agc_driver_image_table_vouched_lookups agc_driver_image_table_vouch_queued_draws PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_shader_type_zero_image_materialization COMMAND agc_shader_memory_tests --type-zero-image-materialization) set_tests_properties(agc_shader_type_zero_image_materialization PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") @@ -2492,6 +2549,12 @@ if(BUILD_TESTING) add_test(NAME agc_shader_frontend_pair_off COMMAND agc_shader_memory_tests --frontend-pair) set_tests_properties(agc_shader_frontend_pair PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_FRONTEND_PAIR=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_result_memo_budget COMMAND agc_shader_memory_tests --result-memo-budget) + set_tests_properties(agc_shader_result_memo_budget PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_MASKED_RESULT_MEMO=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_result_memo_budget_threads COMMAND agc_shader_memory_tests --result-memo-budget-threads) + set_tests_properties(agc_shader_result_memo_budget_threads PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_MASKED_RESULT_MEMO=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_result_memo_budget_masked COMMAND agc_shader_memory_tests --result-memo-budget-masked) + set_tests_properties(agc_shader_result_memo_budget_masked PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_layout_pair COMMAND agc_shader_memory_tests --layout-pair) add_test(NAME agc_shader_layout_pair_off COMMAND agc_shader_memory_tests --layout-pair) set_tests_properties(agc_shader_layout_pair PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") @@ -2499,7 +2562,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_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 PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() foreach(agcTest IN ITEMS agc_vertex_input_tests agc_mesh_output_budget_tests agc_shared_write_tests agc_locks_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vertex_input_validation_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_barrier_batch_tests agc_shader_disk_cache_tests agc_screenshot_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests agc_f16_single_convert_tests) if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests" OR agcTest STREQUAL "agc_driver_rcp_hardware_tests") @@ -2721,6 +2784,11 @@ if(BUILD_TESTING) set_tests_properties(agc_pipeline_journal_long_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_pipeline_library_clear_waits_for_build COMMAND agc_driver_graphics_tests --pipeline-library-clear-waits-for-build) add_test(NAME agc_pipeline_store_lazy_buffer_pool COMMAND agc_driver_graphics_tests --pipeline-store-lazy-buffer-pool) + add_test(NAME agc_pipeline_store_growth COMMAND agc_driver_graphics_tests --pipeline-store-growth) + add_test(NAME agc_pipeline_store_fixed_bound COMMAND agc_driver_graphics_tests --pipeline-store-growth) + add_test(NAME agc_pipeline_store_replay_growth COMMAND agc_driver_graphics_tests --pipeline-store-replay-growth) + set_tests_properties(agc_pipeline_store_growth agc_pipeline_store_fixed_bound agc_pipeline_store_replay_growth PROPERTIES TIMEOUT 60 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_pipeline_store_fixed_bound PROPERTIES ENVIRONMENT "APS5_PIPELINE_STORE_ENTRIES=1024") set_tests_properties(agc_pipeline_journal PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_pipeline_library_clear_waits_for_build PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_pipeline_store_lazy_buffer_pool PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") @@ -2728,7 +2796,7 @@ if(BUILD_TESTING) add_test(NAME agc_pipeline_journal_extended_paths COMMAND agc_driver_graphics_tests --pipeline-journal-extended-paths) set_tests_properties(agc_pipeline_journal_extended_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_pipeline_journal_long_paths agc_pipeline_journal_extended_paths PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") - set_tests_properties(agc_pipeline_journal agc_pipeline_library_clear_waits_for_build agc_pipeline_store_lazy_buffer_pool PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_pipeline_journal agc_pipeline_library_clear_waits_for_build agc_pipeline_store_lazy_buffer_pool agc_pipeline_store_growth agc_pipeline_store_fixed_bound agc_pipeline_store_replay_growth PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30) if(WIN32) @@ -2756,6 +2824,12 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test(NAME agc_address_copy_cap COMMAND agc_driver_graphics_tests --address-copy-cap) + add_test(NAME agc_address_copy_cap_remembered COMMAND agc_driver_graphics_tests --address-copy-cap-remembered) + set_tests_properties(agc_address_copy_cap agc_address_copy_cap_remembered PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_address_copy_cap agc_address_copy_cap_remembered PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_emulated_color_compare_srgb_decode COMMAND agc_driver_emulated_color_compare_tests) set_tests_properties(agc_driver_emulated_color_compare_srgb_decode PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SRGB_SHADER_DECODE=1" @@ -2854,12 +2928,16 @@ if(BUILD_TESTING) set_tests_properties(agc_structurizer_iteration_selection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_ITERATION_SELECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_control_flow_iteration_selection_off COMMAND agc_shader_control_flow_tests) set_tests_properties(agc_shader_control_flow_iteration_selection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_ITERATION_SELECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_structurizer_dominator_tree_off COMMAND agc_structurizer_tests) + set_tests_properties(agc_structurizer_dominator_tree_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_DOMINATOR_TREE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_control_flow_dominator_tree_off COMMAND agc_shader_control_flow_tests) + set_tests_properties(agc_shader_control_flow_dominator_tree_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_DOMINATOR_TREE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_structurizer_forced_fallback COMMAND agc_structurizer_tests) set_tests_properties(agc_structurizer_forced_fallback PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_FORCE_STRUCTURIZER_FALLBACK=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_control_flow_forced_fallback COMMAND agc_shader_control_flow_tests) set_tests_properties(agc_shader_control_flow_forced_fallback PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_FORCE_STRUCTURIZER_FALLBACK=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_structurizer_intersection_off agc_structurizer_iteration_selection_off agc_shader_control_flow_iteration_selection_off agc_structurizer_forced_fallback agc_shader_control_flow_forced_fallback PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_structurizer_intersection_off agc_structurizer_iteration_selection_off agc_shader_control_flow_iteration_selection_off agc_structurizer_dominator_tree_off agc_shader_control_flow_dominator_tree_off agc_structurizer_forced_fallback agc_shader_control_flow_forced_fallback PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_gated_dispatch_capture_short_timeout COMMAND agc_driver_gated_dispatch_capture_tests) set_tests_properties(agc_driver_gated_dispatch_capture_short_timeout PROPERTIES @@ -3118,7 +3196,7 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_array_layer_target_cpu PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable registered_workgroup registered_workgroup_sync abi_deferred_compute abi_deferred_pixel abi_deferred_stages abi_deferred_rectangle abi_deferred_undecodable_state abi_deferred_registration_failure abi_deferred_vertex_inputs abi_deferred_rectangle_pairs lazy lazy_sync) + foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable registered_workgroup registered_workgroup_sync dispatch_ahead_prewarm abi_deferred_compute abi_deferred_pixel abi_deferred_stages abi_deferred_rectangle abi_deferred_undecodable_state abi_deferred_registration_failure abi_deferred_vertex_inputs abi_deferred_rectangle_pairs lazy lazy_sync) set(preparedTest "agc_driver_prepared_shaders_${preparedMode}") if(preparedMode MATCHES "^abi_deferred_") string(REPLACE "_" "-" preparedArgument ${preparedMode}) @@ -3133,6 +3211,8 @@ if(BUILD_TESTING) add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --lazy) elseif(preparedMode STREQUAL "registered_workgroup" OR preparedMode STREQUAL "registered_workgroup_sync") add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --registered-workgroup) + elseif(preparedMode STREQUAL "dispatch_ahead_prewarm") + add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --dispatch-ahead-prewarm) elseif(preparedMode STREQUAL "debug_failure") add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --fail-before-registration) set_tests_properties(${preparedTest} PROPERTIES WILL_FAIL TRUE TIMEOUT 30) @@ -3433,6 +3513,12 @@ if(BUILD_TESTING) TIMEOUT 120) set_tests_properties(agc_driver_frame_replay PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + add_test(NAME agc_driver_frame_replay_concurrent COMMAND agc_driver_frame_replay_tests $ concurrent) + set_tests_properties(agc_driver_frame_replay_concurrent PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_FRAME_CAPTURE=${CMAKE_CURRENT_BINARY_DIR}/frame-replay-concurrent-capture;APS5_FRAME_CAPTURE_AT_FLIP=1;APS5_FRAME_CAPTURE_FRAMES=8" + TIMEOUT 120) + set_tests_properties(agc_driver_frame_replay_concurrent PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() endif() @@ -3956,6 +4042,6 @@ endif() if(BUILD_TESTING) get_directory_property(agcRegisteredTests TESTS) - list(FILTER agcRegisteredTests EXCLUDE REGEX "^(agc_pipeline_journal(_long_paths|_extended_paths)?|agc_driver_pipeline_journal)$") + list(FILTER agcRegisteredTests EXCLUDE REGEX "^(agc_pipeline_journal(_long_paths|_extended_paths)?|agc_driver_pipeline_journal|agc_pipeline_store_replay_growth)$") set_property(TEST ${agcRegisteredTests} APPEND PROPERTY ENVIRONMENT "APS5_NO_PIPELINE_JOURNAL=1") endif() diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp index e02db9f381..03e68cd1b7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp @@ -33,6 +33,8 @@ extern "C" void AgcDriverSuspendPoint_nid_postfix(); extern "C" void AgcDriverSerialProgress_nid_postfix(std::uint64_t* accepted, std::uint64_t* completed); extern "C" std::size_t AgcDriverAwaitedWaits_nid_postfix(AgcDriver::AwaitedWait* waits, std::size_t capacity); extern "C" std::uint64_t AgcDriverDispatchesAdoptedAhead_nid_postfix(); +extern "C" std::uint64_t AgcDriverDispatchesRejectedAhead_nid_postfix(); +extern "C" void AgcDriverSetAheadAdoptHook_nid_postfix(void (*hook)()); extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader); extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span context, std::span primitive); extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span stages, std::span context, std::span primitive); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp index 39bb6c35c9..86505f5a7b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp @@ -10,7 +10,7 @@ namespace AgcDriver::DriverDetail { double TraceMs(); -std::uint64_t SkippedShader(); +bool SkippedShader(std::uint64_t address); class RefusedDraw final : public std::runtime_error { public: diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp index 50844922e8..f2bc4960a5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp @@ -55,6 +55,7 @@ public: std::uint64_t DecodeDrains() const { return decodeDrains.load(std::memory_order_relaxed); } std::optional PendingLabel(std::uint64_t address, std::size_t bytes); static bool HeldReads(std::uint64_t address, std::size_t bytes); + static std::uint64_t HeldReadQueries(); void ReleaseRunningReads(); static bool MetadataPassesPipelined(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 18303f9f7b..8951aa78e1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -59,6 +59,8 @@ public: void Shutdown(); void WaitIdle(); std::uint64_t DispatchesAdoptedAhead() const; + std::uint64_t DispatchesRejectedAhead() const; + void SetAheadAdoptHook(void (*hook)()); void CheckFailure(); void ReportFailure(std::exception_ptr error); void Submit(const Packet* packet, std::uint32_t queue, bool cpuFlip = false); @@ -74,7 +76,7 @@ public: void RegisterVideoOutput(std::uint32_t handle, const std::shared_ptr& output); void UnregisterVideoOutput(std::uint32_t handle, const std::shared_ptr& output); void AttachWindow(const PresentationWindow& window); - void Present(const PresentationWindow& window, const DisplayBuffer* buffer, bool opaque, void (*gpuReady)(void*), void* context); + void Present(const PresentationWindow& window, const DisplayBuffer* buffer, const DisplayBuffer* plane, bool opaque, void (*gpuReady)(void*), void* context); void ReleaseWindow(void* window); void RegisterShader(const Shader* shader); void ResolveShaderAbi(const Shader* shader, std::span context, std::span primitive); @@ -317,7 +319,7 @@ private: std::chrono::steady_clock::time_point resolvedAt{}; }; struct GroupCaptureStats { - std::uint64_t runs = 0, resolved = 0, resolvedHits = 0, adopted = 0, addressMismatches = 0, keyMismatches = 0, failures = 0, stale = 0, labelOverlaps = 0, writtenBefore = 0, deviceRejects = 0, argumentMismatches = 0; + std::uint64_t runs = 0, resolved = 0, resolvedHits = 0, adopted = 0, addressMismatches = 0, keyMismatches = 0, memoryMismatches = 0, failures = 0, stale = 0, labelOverlaps = 0, writtenBefore = 0, deviceRejects = 0, argumentMismatches = 0; double resolveMs = 0, resolveMaxMs = 0, ageMs = 0, ageMaxMs = 0; std::chrono::steady_clock::time_point reported = std::chrono::steady_clock::now(); }; @@ -474,6 +476,8 @@ private: std::atomic evidenceReads{0}; std::atomic evidenceValidations{0}; std::atomic dispatchesAdoptedAhead{0}; + std::atomic dispatchesRejectedAhead{0}; + std::atomic aheadAdoptHook{nullptr}; AgcDriver::Mutex writtenBuffersMutex; std::deque writtenBuffers; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/FrameRecorder.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/FrameRecorder.hpp index bad806af09..e4ed5cf19b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/FrameRecorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/FrameRecorder.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -35,10 +36,10 @@ public: void ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType); void RegisterVideoOutput(std::uint32_t handle); void UnregisterVideoOutput(std::uint32_t handle); - void Submit(const Packet& packet, std::uint32_t queue); - void Flip(const FlipInfo& info); + void Submit(const Packet& packet, std::uint32_t queue, const std::function& accept); + void Flip(const FlipInfo& info, const std::function& accept); void ReserveEopFlip(const FlipInfo& info, std::uint32_t token); - void Suspend(); + void Suspend(const std::function& accept); void PresentBuffer(const DisplayBuffer& buffer); void FrameBoundary(); @@ -62,7 +63,8 @@ private: }; FrameCall make(FrameCallKind kind); - void record(FrameCall call, bool prologueCall, std::span shaders = {}); + void record(FrameCall call, bool prologueCall, std::span shaders = {}, const std::function& accept = {}); + void recordLocked(FrameCall call, bool prologueCall, std::span shaders); void emitDelta(); void emitShaderDelta(std::span shaders); void writeDelta(std::span spans, std::span hostPages, std::span added = {}); @@ -72,6 +74,7 @@ private: void snapshot(const Range& range); void writeReferenceFrames(); bool abandonIfDiskLow(); + void stop(const char* reason); std::string header() const; std::vector committedRanges(); std::vector newRanges(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index c360a6a579..67170acc07 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -63,7 +63,7 @@ void WriteChanged(std::uint64_t address, std::span current, std bool WriteWatched(); void Unwatch(std::uint64_t address, std::size_t bytes); void Rewatch(std::span> ranges); -bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function& import, bool staysWatched = true); +bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function& import, bool staysWatched = true, bool importMayWriteGuest = true); void SetUnwatchHook(void (*hook)(std::uint64_t address, std::size_t bytes)); #ifdef _WIN32 using FreshCollectHook = bool (*)(std::uintptr_t address, std::size_t bytes, void (*written)(void*, std::uintptr_t, std::uintptr_t), void* context); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp index e683bc7695..49858aaa31 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp @@ -451,18 +451,30 @@ public: static void AppendString(std::string& output, std::string_view text) { output += '"'; - for (const char character : text) { + std::size_t index = 0; + while (index < text.size()) { + const char character = text[index]; const auto byte = static_cast(character); if (character == '"' || character == '\\') { output += '\\'; output += character; - } else if (byte < 0x20 || byte >= 0x7f) { + } else if (byte >= 0x80) { + if (const auto length = utf8SequenceLength(text, index); length != 0) { + output.append(text.substr(index, length)); + index += length; + continue; + } + char escaped[8]; + std::snprintf(escaped, sizeof escaped, "\\u%04x", static_cast(byte)); + output += escaped; + } else if (byte < 0x20 || byte == 0x7f) { char escaped[8]; std::snprintf(escaped, sizeof escaped, "\\u%04x", static_cast(byte)); output += escaped; } else { output += character; } + ++index; } output += '"'; } @@ -697,6 +709,34 @@ public: } private: + static std::size_t utf8SequenceLength(std::string_view text, std::size_t index) { + const auto lead = static_cast(text[index]); + std::size_t length = 0; + unsigned char low = 0x80; + unsigned char high = 0xbf; + if (lead >= 0xc2 && lead <= 0xdf) { + length = 2; + } else if (lead >= 0xe0 && lead <= 0xef) { + length = 3; + if (lead == 0xe0) low = 0xa0; + if (lead == 0xed) high = 0x9f; + } else if (lead >= 0xf0 && lead <= 0xf4) { + length = 4; + if (lead == 0xf0) low = 0x90; + if (lead == 0xf4) high = 0x8f; + } else { + return 0; + } + if (text.size() - index < length) return 0; + for (std::size_t offset = 1; offset < length; ++offset) { + const auto next = static_cast(text[index + offset]); + if (next < low || next > high) return 0; + low = 0x80; + high = 0xbf; + } + return length; + } + static void AppendMachine(std::string& output, const TraceSummary& summary) { if (summary.host) { output += ",\"cpu\":"; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PresentMode.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PresentMode.hpp index cb448bbc44..17510d198b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PresentMode.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PresentMode.hpp @@ -3,6 +3,8 @@ #include #include +#include +#include #include #include #include @@ -22,6 +24,11 @@ inline VkPresentModeKHR SelectPresentMode(const char* requested, std::span SelectPresentQueue(std::uint32_t familyQueues, std::uint32_t queuesTaken) { + if (queuesTaken >= familyQueues) return std::nullopt; + return queuesTaken; +} + } #endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp index 3cdc977947..e6b4160310 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp @@ -26,8 +26,8 @@ struct PresentationWindow { } extern "C" void AgcDriverAttachWindow_nid_postfix(const AgcDriver::PresentationWindow& window); -extern "C" void AgcDriverPresentClear_nid_postfix(const AgcDriver::PresentationWindow& window, bool opaque, void (*gpuReady)(void*), void* context); -extern "C" void AgcDriverPresentBuffer_nid_postfix(const AgcDriver::PresentationWindow& window, const AgcDriver::DisplayBuffer& buffer, void (*gpuReady)(void*), void* context); +extern "C" void AgcDriverPresentClear_nid_postfix(const AgcDriver::PresentationWindow& window, bool opaque, const AgcDriver::DisplayBuffer* plane, void (*gpuReady)(void*), void* context); +extern "C" void AgcDriverPresentBuffer_nid_postfix(const AgcDriver::PresentationWindow& window, const AgcDriver::DisplayBuffer& buffer, const AgcDriver::DisplayBuffer* plane, void (*gpuReady)(void*), void* context); extern "C" void AgcDriverReleaseWindow_nid_postfix(void* window); extern "C" void AgcDriverTogglePerformanceOverlay_nid_postfix(); extern "C" void AgcDriverReportFailure_nid_postfix(std::exception_ptr error); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp index 337bfb0805..319dc9c15e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp @@ -4,9 +4,12 @@ #define VK_NO_PROTOTYPES #include #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include #include #include #include +#include +#include #include #include @@ -24,8 +27,14 @@ public: PresentationPass& operator=(const PresentationPass&) = delete; void Bind(std::size_t slot, VkImage image, std::uint32_t width, std::uint32_t height, std::uint64_t pixelFormat, float whiteNits, std::uint32_t layer = 0); + void BindTexels(std::size_t slot, VkCommandBuffer commands, std::span texels, std::uint32_t width, std::uint32_t height, std::uint64_t pixelFormat, float whiteNits); void RecordPresent(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout finalLayout); + void RecordOverlay(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout); void RecordDump(VkCommandBuffer commands, std::size_t slot, std::uint32_t stride, VkBuffer destination); + void RecordDumpImage(VkCommandBuffer commands, std::size_t slot, std::uint32_t stride); + void RecordDumpReadback(VkCommandBuffer commands, std::size_t slot, VkBuffer destination); + VkImage DumpImage(std::size_t slot) const; + VkExtent2D DumpImageExtent(std::size_t slot) const; void Release(std::size_t slot) noexcept; static bool TenBitFormat(VkFormat format); static VkExtent2D DumpExtent(std::uint32_t width, std::uint32_t height, std::uint32_t stride); @@ -48,14 +57,21 @@ private: VkImageView dumpView = VK_NULL_HANDLE; VkFramebuffer dumpFramebuffer = VK_NULL_HANDLE; VkExtent2D dumpExtent{}; + VkImage uploadImage = VK_NULL_HANDLE; + VkDeviceMemory uploadMemory = VK_NULL_HANDLE; + VkExtent2D uploadExtent{}; + std::unique_ptr uploadTexels; }; - const Target& target(VkFormat format, VkImageLayout finalLayout); + const Target& target(VkFormat format, VkImageLayout finalLayout, VkImageLayout loadFrom = VK_IMAGE_LAYOUT_UNDEFINED); VkImageView createView(VkImage image, VkFormat format, VkImageUsageFlags usage, std::uint32_t layer = 0) const; VkFramebuffer createFramebuffer(VkRenderPass renderPass, VkImageView view, VkExtent2D extent) const; + VkFramebuffer destinationFramebuffer(Slot& slot, const Target& target, VkImage destination, VkFormat format, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout); void ensureDumpTarget(Slot& slot, VkExtent2D extent); void releaseDumpTarget(Slot& slot) noexcept; - void draw(VkCommandBuffer commands, const Slot& slot, const Target& target, VkFramebuffer framebuffer, VkExtent2D extent, const std::int32_t (¶meters)[9]) const; + void ensureUploadImage(Slot& slot, VkExtent2D extent); + void releaseUploadImage(Slot& slot) noexcept; + void draw(VkCommandBuffer commands, const Slot& slot, const Target& target, VkFramebuffer framebuffer, VkExtent2D extent, VkRect2D scissor, const std::int32_t (¶meters)[9]) const; void release() noexcept; Graphics::Context context; @@ -66,7 +82,7 @@ private: VkShaderModule vertex = VK_NULL_HANDLE; VkShaderModule fragment = VK_NULL_HANDLE; std::unique_ptr pqNits; - std::map, Target> targets; + std::map, Target> targets; std::vector slots; }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp index b0e88120aa..9b05f46fb4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp @@ -36,6 +36,8 @@ public: static std::vector LetterboxBands(std::uint32_t width, std::uint32_t height, std::uint32_t destinationWidth, std::uint32_t destinationHeight); std::uint32_t SourceWidth() const { return sourceWidth; } std::uint32_t SourceHeight() const { return sourceHeight; } + VkImage SourceImage() const { return sourceImage; } + VkFormat SourceFormat() const { return sourceFormat; } private: void release() noexcept; @@ -46,6 +48,8 @@ private: std::uint32_t sourceHeight = 0; VkImage sourceImage = VK_NULL_HANDLE; VkDeviceMemory sourceMemory = VK_NULL_HANDLE; + VkDeviceSize sourceBytes = 0; + std::uint32_t sourceMemoryType = 0; VkImage blackImage = VK_NULL_HANDLE; VkDeviceMemory blackMemory = VK_NULL_HANDLE; }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/SwapchainImages.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/SwapchainImages.hpp new file mode 100644 index 0000000000..b3411ca19b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/SwapchainImages.hpp @@ -0,0 +1,17 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SWAPCHAINIMAGES_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SWAPCHAINIMAGES_HPP + +#include +#include +#include + +namespace AgcDriver { + +inline std::uint32_t SwapchainImageCount(const VkSurfaceCapabilitiesKHR& surface) { + const std::uint32_t count = std::max(surface.minImageCount, 3u); + return surface.maxImageCount != 0 ? std::min(count, surface.maxImageCount) : count; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/TessellationPipelineCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/TessellationPipelineCache.hpp new file mode 100644 index 0000000000..7605cd2c68 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/TessellationPipelineCache.hpp @@ -0,0 +1,14 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_TESSELLATIONPIPELINECACHE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_TESSELLATIONPIPELINECACHE_HPP + +#include + +namespace AgcDriver { + +inline bool TessellationPipelineCacheUsable(VkDriverId driver) { + return driver != VK_DRIVER_ID_MOLTENVK; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 8dbf80c316..1e380f74b4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -45,9 +45,11 @@ struct DeviceQueuePlan { bool sparseBinding = false; std::uint32_t sparseFamily = VK_QUEUE_FAMILY_IGNORED; std::uint32_t sparseIndex = 0; + bool present = false; + std::uint32_t presentIndex = 0; }; -DeviceQueuePlan PlanDeviceQueues(std::span families, std::uint32_t family, bool settles, bool sparseBinding); +DeviceQueuePlan PlanDeviceQueues(std::span families, std::uint32_t family, bool settles, bool sparseBinding, bool presents); class VulkanDevice { public: @@ -58,6 +60,8 @@ public: // Builds the pipeline of a journaled compute result ahead of its first dispatch (the journal // replay's hook, on its threads): false when the device already holds a pipeline for the module. bool ReplayComputePipeline(const ShaderRecompiler::RecompileResult& shader, bool pushConstants); + bool HasComputePipeline(const ShaderRecompiler::RecompileResult& shader) const; + void PrebuildComputePipeline(const ShaderRecompiler::RecompileResult& shader, std::uint64_t programAddress = 0); // Starts the journal replay if the device has not (a headless device starts it at its first // draw or dispatch) and joins it (tests; the teardown joins it anyway). void WaitForPipelineJournalReplay(); @@ -226,7 +230,7 @@ public: bool ProvokingVertexLast() const; bool GraphicsPipelineLibraries() const; // A presentation is a few steps so the presenter holds GuestMemory::GpuMutex only while it - // touches the queue. Presentations are slots (FlipInFlight() + 1, each with its own command + // touches the main queue. Presentations are slots (FlipInFlight() + 1, each with its own command // buffer, fence, kept resident image and dump buffer): RetirePresents(keep) (no mutex) retires // the slots whose fence signaled, oldest first, and waits for the oldest ones until at most // `keep` are in flight (the only CPU wait of the flip path; it returns the time spent waiting); @@ -235,7 +239,9 @@ public: // PresentClear/PresentDisplayBuffer record and submit the frame into the next slot (under the // mutex; the blit follows the frame's batches in queue order, so the displayed image is always // complete) and return whether one was submitted; QueuePresent hands the image to the swapchain - // (under the mutex). FinishPresent = RetirePresents(0): the render fence wait of the + // (on the device's presentation queue under that queue's mutex, with the GPU mutex released, + // when the presentation family had a free queue; otherwise on the main queue under the GPU + // mutex). FinishPresent = RetirePresents(0): the render fence wait of the // synchronous paths (APS5_FLIP_INFLIGHT=0, APS5_SYNC_FLIP, PresentPixels). AcquireImage returns // false when the swapchain is out of date (the frame is dropped; the next Resize recreates it); // a present without a prior AcquireImage acquires itself, one whose slot is in flight waits for @@ -253,11 +259,12 @@ public: // mutex (the same wait under it is only the fallback when the answer changes in between). bool PresentWaitsForSlots(const DisplayBuffer* buffer) const; bool AcquireImage(); - bool PresentClear(std::uint32_t width, std::uint32_t height, bool opaque); + bool PresentClear(std::uint32_t width, std::uint32_t height, bool opaque, const DisplayBuffer* plane = nullptr); void PresentPixels(std::uint32_t width, std::uint32_t height, std::span pixels); - bool PresentDisplayBuffer(const DisplayBuffer& buffer); + bool PresentDisplayBuffer(const DisplayBuffer& buffer, const DisplayBuffer* plane = nullptr); double FinishPresent(); void QueuePresent(); + bool PresentsOnOwnQueue() const; // At the flip packet (under the mutex): the recorder's submissions so far and, under // APS5_PROFILE_DRAW, how many in flight have not signaled (the frame record's batches_at_flip). void FlipBatches(std::uint64_t& submissions, std::uint64_t& unsignaled) const; @@ -268,8 +275,8 @@ public: // blit-submit times after the flip packet; noted in-place reads checked // (APS5_FLIP_READ_CHECK=1) and found overwritten before the blit's fence. struct PresentStatistics { - std::uint64_t presents, gpuUnread, batchesAheadOfBlit, batchesAfterFlip, readsChecked, readsOverwritten, acquireDrops; - double gpuBusyMs, gpuGapMs, lastSubmitAfterFlipMs, blitSubmitAfterFlipMs; + std::uint64_t presents, gpuUnread, batchesAheadOfBlit, batchesAfterFlip, readsChecked, readsOverwritten, acquireDrops, planes, keyWriterWaits; + double gpuBusyMs, gpuGapMs, lastSubmitAfterFlipMs, blitSubmitAfterFlipMs, keyWriterWaitMs; }; static PresentStatistics PresentCounts(); struct PresyncStatistics { @@ -385,7 +392,7 @@ private: // The tail of a dispatch's device call from the open batch's command buffer to the completion // registration: keeps, the template data refresh, barriers, bind, push, dispatch, marks. void recordDispatch(RecordedDispatch& record); - bool present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display = nullptr, const std::shared_ptr& resident = nullptr, VkFilter residentFilter = VK_FILTER_LINEAR, bool dumpFrame = false, bool residentConvert = false, const VkClearColorValue* uniform = nullptr, std::uint32_t residentLayer = 0, bool passthrough = false); + bool present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display = nullptr, const std::shared_ptr& resident = nullptr, VkFilter residentFilter = VK_FILTER_LINEAR, bool dumpFrame = false, bool residentConvert = false, const VkClearColorValue* uniform = nullptr, std::uint32_t residentLayer = 0, bool passthrough = false, const DisplayBuffer* plane = nullptr); bool takeScreenshot(bool dumpFrame); void recreateSwapchain(std::uint32_t width, std::uint32_t height, bool force); void refreshSurfaceFormats(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp index b94858e431..ca92adc259 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp @@ -1,11 +1,19 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include +#include +#include +#include #include #include +#include +#include #include +#include #include #include #include +#include namespace AgcDriver::DriverDetail { @@ -14,9 +22,65 @@ double TraceMs() { return std::chrono::duration(std::chrono::steady_clock::now() - origin).count(); } -std::uint64_t SkippedShader() { - static const std::uint64_t skipped = [] { const char* text = std::getenv("APS5_SKIP_SHADER"); return text ? std::strtoull(text, nullptr, 16) : 0ull; }(); - return skipped; +namespace { + +constexpr std::size_t MaxSkippedShaders = 256; +std::array, MaxSkippedShaders> skippedShaders{}; +std::atomic skippedShaderCount{0}; + +std::optional programAddress(std::string_view word) { + if (word.starts_with("0x") || word.starts_with("0X")) word.remove_prefix(2); + std::uint64_t address = 0; + const auto [end, error] = std::from_chars(word.data(), word.data() + word.size(), address, 16); + if (word.empty() || error != std::errc{} || end != word.data() + word.size()) return std::nullopt; + return address; +} + +void ReloadSkippedShaders(const char* path) { + static std::mutex reload; + static std::filesystem::file_time_type seen{}; + const std::unique_lock lock(reload, std::try_to_lock); + if (!lock.owns_lock()) return; + std::error_code error; + const auto stamp = std::filesystem::last_write_time(path, error); + if (error || stamp == seen) return; + seen = stamp; + std::ifstream file(path); + std::size_t count = 0; + skippedShaderCount.store(0, std::memory_order_release); + for (std::string word; count < MaxSkippedShaders && file >> word;) { + if (word.starts_with('#')) { + std::getline(file, word); + continue; + } + const auto address = programAddress(word); + if (!address) { + std::fprintf(stderr, "[skip-shader] ignoring '%s' in %s: not a hexadecimal program address\n", word.c_str(), path); + continue; + } + if (*address != 0) skippedShaders[count++].store(*address, std::memory_order_relaxed); + } + skippedShaderCount.store(count, std::memory_order_release); + std::fprintf(stderr, "[skip-shader] %zu programs listed in %s\n", count, path); +} + +} + +bool SkippedShader(std::uint64_t address) { + static const std::uint64_t single = [] { const char* text = std::getenv("APS5_SKIP_SHADER"); return text ? std::strtoull(text, nullptr, 16) : 0ull; }(); + static const char* const file = std::getenv("APS5_SKIP_SHADER_FILE"); + if (address == 0) return false; + if (address == single) return true; + if (file == nullptr) return false; + static std::atomic nextCheck{0}; + const auto now = std::chrono::steady_clock::now().time_since_epoch(); + auto due = nextCheck.load(std::memory_order_relaxed); + if (now.count() >= due && nextCheck.compare_exchange_strong(due, std::chrono::duration_cast(now + std::chrono::milliseconds(500)).count(), std::memory_order_relaxed)) ReloadSkippedShaders(file); + const auto count = skippedShaderCount.load(std::memory_order_acquire); + for (std::size_t index = 0; index < count; ++index) { + if (skippedShaders[index].load(std::memory_order_relaxed) == address) return true; + } + return false; } bool SkipRefusedDraws() { 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 8110338e83..a08d54121c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -16,6 +16,7 @@ #include "Optimization/ResourceProgram.hpp" #include #include +#include #include #include #include @@ -57,6 +58,19 @@ void ReportDispatchItems(bool profile) { std::fprintf(stderr, "[dispatch-item] %llu commit items (%llu recipes rebuilt on the committer); drained instead, cumulative:%s\n", static_cast(itemCommits.load(std::memory_order_relaxed)), static_cast(itemRebuilds.load(std::memory_order_relaxed)), drained.c_str()); } +bool CapturedCurrent(std::span captured, const ShaderSnapshot& code) { + for (const auto& region : captured) { + const auto address = region.guestAddress; + const auto bytes = region.bytes.size(); + if (bytes == 0 || (address >= code.codeAddress && address < code.codeAddress + code.code.size() * sizeof(std::uint32_t)) || (!code.header.empty() && address >= code.headerAddress && address < code.headerAddress + code.header.size())) continue; + if (Driver::QueryPendingSpan(address, bytes, {}) != ShaderMemory::PendingWrite::None || !GuestMemory::Accessible(reinterpret_cast(address), bytes)) return false; + GuestMemory::GuardRead(address, bytes); + GuestMemory::FlushGpuWrites(address, bytes); + if (std::memcmp(region.bytes.data(), reinterpret_cast(address), bytes) != 0) return false; + } + return true; +} + } bool Driver::pipelineDispatch() { @@ -66,7 +80,7 @@ bool Driver::pipelineDispatch() { void Driver::dispatch(QueueState& queue, std::span packet, const Submission& submission, std::uint64_t indirectArguments) { PerformanceTimer timing("Driver.Dispatch"); const auto address = (static_cast(readRegister(queue.shader, 0x20c)) << 8u) | (static_cast(readRegister(queue.shader, 0x20d) & 0xffu) << 40u); - if (address == SkippedShader() && address != 0) return; + if (SkippedShader(address)) return; auto registeredShader = RegisteredProgram(*submission.shaders, address, {0}); if (!registeredShader) registeredShader = ReadRawShader(address); const auto& snapshot = *registeredShader; @@ -281,6 +295,14 @@ void Driver::dispatch(QueueState& queue, std::span packet, fromAhead = false; ++groupStats.keyMismatches; } + if (fromAhead) { + if (const auto hook = aheadAdoptHook.load(std::memory_order_acquire)) hook(); + if (!CapturedCurrent(ahead.captured, snapshot)) { + fromAhead = false; + ++dispatchesRejectedAhead; + ++groupStats.memoryMismatches; + } + } phaseTiming.Phase(PhaseKey); timing.Mark("decode_key_device_setup"); bool readsClear = false; @@ -414,6 +436,11 @@ void Driver::dispatch(QueueState& queue, std::span packet, parked.resolvedAt = std::chrono::steady_clock::now(); ++groupStats.resolved; if (cached) ++groupStats.resolvedHits; + if (!cached && compiledResult != nullptr && !localDevice->HasComputePipeline(*compiledResult)) { + SubmitLibraryPrebuild([localDevice, compiledResult, address] { + try { localDevice->PrebuildComputePipeline(*compiledResult, address); } catch (const std::exception&) {} + }); + } return; } if (verifyDataHits() && dataHit) verifyDataHit(snapshot, codeOffset, request, memory, address, *keepVariant, liveWords, *compiledResult); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp index 6391881462..50a5eee57b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp @@ -2,9 +2,11 @@ #include "ThreadOwned.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" +#include "Optimization/ResourceProgram.hpp" #include #include #include @@ -40,7 +42,25 @@ std::uint64_t Driver::DispatchesAdoptedAhead() const { return dispatchesAdoptedAhead.load(std::memory_order_relaxed); } +std::uint64_t Driver::DispatchesRejectedAhead() const { + return dispatchesRejectedAhead.load(std::memory_order_relaxed); +} + +void Driver::SetAheadAdoptHook(void (*hook)()) { + aheadAdoptHook.store(hook, std::memory_order_release); +} + +namespace { + +std::size_t& PreviewedUntil() { + static thread_local std::size_t until = 0; + return until; +} + +} + void Driver::clearResolvedAhead() { + PreviewedUntil() = 0; auto& resolved = resolvedAhead(); if (resolved.empty()) return; groupCaptureStats().stale += resolved.size(); @@ -107,7 +127,8 @@ void Driver::resolveGroupAhead(const Submission& submission, const QueueState& l static const bool allQueues = std::getenv("APS5_GROUP_CAPTURE_ALL_QUEUES") != nullptr; if (!allQueues && submission.queue == 0) return; auto& resolved = resolvedAhead(); - if (resolved.contains(from) || device.Load() == nullptr) return; + const auto localDevice = device.Load(); + if (resolved.contains(from) || localDevice == nullptr) return; if (submission.queue == 0 && DrawPipeline::Depth() != 0 && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, (submission.commands[from] >> 8u) & 0xffu); auto& stats = groupCaptureStats(); const auto begin = std::chrono::steady_clock::now(); @@ -126,6 +147,110 @@ void Driver::resolveGroupAhead(const Submission& submission, const QueueState& l } } resolving; const auto& commands = submission.commands; + if (submission.shaders != nullptr && from >= PreviewedUntil()) { + bool hasFurtherDispatch = false; + std::size_t limit = from < commands.size() ? from + Pm4::PacketWords(commands[from]) : commands.size(); + while (limit < commands.size()) { + const auto header = commands[limit]; + if (Pm4::FillerPacket(header)) { + ++limit; + continue; + } + const auto count = Pm4::PacketWords(header); + if (limit + count > commands.size()) break; + const auto opcode = (header >> 8u) & 0xffu; + if (header == FlipPacketHeader || header == RenderingWaitPacketHeader || opcode == 0x22 || opcode == 0x3f || opcode == 0x3c || opcode == 0x93) break; + if (opcode == 0x15 || opcode == 0x16) hasFurtherDispatch = true; + limit += count; + } + PreviewedUntil() = limit; + if (hasFurtherDispatch) { + static thread_local QueueState* previewSlot = nullptr; + auto& preview = ShaderRecompiler::ThreadOwned(previewSlot); + preview.shader = live.shader; + preview.predication = live.predication; + std::vector pendingShaders; + std::vector submittedShaders; + std::size_t scan = from; + while (scan < limit) { + const auto header = commands[scan]; + if (Pm4::FillerPacket(header)) { + ++scan; + continue; + } + const auto count = Pm4::PacketWords(header); + if (scan + count > limit) break; + const auto packet = std::span(commands).subspan(scan, count); + const auto opcode = (header >> 8u) & 0xffu; + const auto next = scan + count; + if (opcode == 0x15 || opcode == 0x16) { + if (count < (opcode == 0x15 ? 5u : 3u)) break; + if (scan > from) { + const auto low = preview.shader.find(0x20c); + const auto high = preview.shader.find(0x20d); + if (low != preview.shader.end() && high != preview.shader.end()) { + const auto address = (static_cast(low->second) << 8u) | (static_cast(high->second & 0xffu) << 40u); + if (auto registered = RegisteredProgram(*submission.shaders, address, {0}); registered != nullptr && registered->type == 0) { + bool isPending = false; + bool isEmpty = false; + { + std::lock_guard lock(registered->prepared->mutex); + isPending = registered->prepared->pending; + isEmpty = registered->prepared->entries.empty(); + } + if (isPending) { + if (std::ranges::find(pendingShaders, registered->prepared.get()) == pendingShaders.end()) { + pendingShaders.push_back(registered->prepared.get()); + } + } else if (isEmpty && std::ranges::find(submittedShaders, registered->prepared.get()) == submittedShaders.end()) { + const auto codeOffset = static_cast((address - registered->codeAddress) / sizeof(std::uint32_t)); + if (codeOffset < registered->code.size()) { + try { + auto compute = Graphics::DecodeComputeStageInfo(preview.shader, registered->header); + submittedShaders.push_back(registered->prepared.get()); + const auto rsrc2 = preview.shader.find(0x213); + const auto userCount = rsrc2 != preview.shader.end() ? ((rsrc2->second >> 1u) & 0x1fu) : 0u; + std::vector userData(userCount, 0u); + for (std::uint32_t i = 0; i < userCount; ++i) { + if (const auto reg = preview.shader.find(0x240u + i); reg != preview.shader.end()) userData[i] = reg->second; + } + const auto wave = (packet.back() & 0x8000u) != 0 ? 32u : 64u; + SubmitRegistrationPreparation([registered, localDevice, address, codeOffset, wave, compute, userData = std::move(userData), state = preview.shader]() mutable { + std::vector memory{{registered->codeAddress, std::as_bytes(std::span(registered->code))}}; + if (!registered->header.empty()) memory.push_back({registered->headerAddress, registered->header}); + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, address, std::span(registered->code).subspan(codeOffset), registered->headerAddress, registered->header}, + {wave, 0, userData, compute, std::nullopt, std::nullopt, memory, DispatchFloatMode(*registered, state)}, + localDevice->ComputeTarget(wave), + {0, 0, 0, 128} + }; + try { static_cast(InvocationFor(*registered, codeOffset, request)); } catch (const std::exception&) {} + }, registered->prepared.get()); + } catch (const std::exception&) {} + } + } + } + } + } + Pm4::NoteDispatchRegisters(packet, preview); + } else if (Pm4::IndirectRegisterOpcode(opcode)) { + const auto found = submission.registerLists.find(scan); + if (count != 5 || found == submission.registerLists.end() || found->second.size() != static_cast(packet[4]) * 2 || !Pm4::IndirectRegistersValid(found->second)) break; + Pm4::ExecuteIndirectRegisters(packet, found->second, preview); + } else if (!Pm4::AccessesMemory(header) && opcode != 0x3c && opcode != 0x93 && opcode != 0x37 && opcode != 0x42 && opcode != 0x46 && opcode != 0x49 && opcode != 0x58) { + try { + Pm4::Execute(packet, preview); + } catch (const std::exception&) { + break; + } + } + scan = next; + } + for (auto it = pendingShaders.rbegin(); it != pendingShaders.rend(); ++it) { + PrioritizeRegistrationPreparation(**it); + } + } + } for (std::size_t cursor = from; cursor < commands.size();) { const auto header = commands[cursor]; if (Pm4::FillerPacket(header)) { @@ -141,7 +266,7 @@ void Driver::resolveGroupAhead(const Submission& submission, const QueueState& l if (opcode == 0x3c || opcode == 0x93) { if (!Pm4::WaitSatisfiedUnchecked(packet) && !WrittenByRun(packet, labels)) break; } else if (opcode == 0x15 || opcode == 0x16) { - if (resolved.contains(cursor)) break; + if (count < (opcode == 0x15 ? 5u : 3u) || resolved.contains(cursor)) break; if (opcode == 0x16) { std::uint64_t arguments = 0; try { @@ -209,9 +334,9 @@ void Driver::reportGroupCapture(std::uint32_t queue) { auto& stats = groupCaptureStats(); const auto now = std::chrono::steady_clock::now(); if (now - stats.reported < std::chrono::seconds(10)) return; - AgcDriver::ProfilePrint_nid_no_patch("[groupcap] queue 0x%x (10 s): %llu runs resolved %llu dispatches (%llu cache hits) in %.1f ms (max %.2f ms); %llu adopted %.2f ms after the resolve on average, max %.2f ms; mismatches: address %llu key %llu; %llu resolve failures, %llu stale, %llu label overlaps, %llu read an earlier dispatch's output, %llu rejected by the device, %llu dropped because the argument re-read differed\n", + AgcDriver::ProfilePrint_nid_no_patch("[groupcap] queue 0x%x (10 s): %llu runs resolved %llu dispatches (%llu cache hits) in %.1f ms (max %.2f ms); %llu adopted %.2f ms after the resolve on average, max %.2f ms; mismatches: address %llu key %llu memory %llu; %llu resolve failures, %llu stale, %llu label overlaps, %llu read an earlier dispatch's output, %llu rejected by the device, %llu dropped because the argument re-read differed\n", queue, static_cast(stats.runs), static_cast(stats.resolved), static_cast(stats.resolvedHits), stats.resolveMs, stats.resolveMaxMs, - static_cast(stats.adopted), stats.adopted != 0 ? stats.ageMs / static_cast(stats.adopted) : 0.0, stats.ageMaxMs, static_cast(stats.addressMismatches), static_cast(stats.keyMismatches), + static_cast(stats.adopted), stats.adopted != 0 ? stats.ageMs / static_cast(stats.adopted) : 0.0, stats.ageMaxMs, static_cast(stats.addressMismatches), static_cast(stats.keyMismatches), static_cast(stats.memoryMismatches), static_cast(stats.failures), static_cast(stats.stale), static_cast(stats.labelOverlaps), static_cast(stats.writtenBefore), static_cast(stats.deviceRejects), static_cast(stats.argumentMismatches)); stats = GroupCaptureStats{}; stats.reported = now; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index 477c943f72..4d85df3c00 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -518,7 +518,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe for (const auto& patch : userPatches) writablePrograms[patch.program].userData[patch.index] = patch.value; tail.programs = writesPrograms ? &writablePrograms : &decode->programs; const auto& programs = *tail.programs; - if (const auto skipped = SkippedShader(); skipped != 0 && std::any_of(programs.begin(), programs.end(), [skipped](const DrawProgram& program) { return program.binary.codeAddress == skipped; })) return drawn(); + if (std::any_of(programs.begin(), programs.end(), [](const DrawProgram& program) { return SkippedShader(program.binary.codeAddress); })) return drawn(); const bool emptyIndirectSkip = EmptyIndirectSkip(); auto& cpuIndirectPrograms = CpuIndirectPrograms(); const auto recordDraws = [&](const Pm4::DrawArguments& arguments) { return arguments.count != 0 && arguments.instances != 0 && (!drawParameters.indexed || !drawParameters.indirect->indexLimitKnown || arguments.firstVertexOrIndex < drawParameters.indexCount); }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index 0672150150..05c6b4dfa2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -183,6 +183,7 @@ std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Su product->pixel = Graphics::DecodePixelStageInfo(queue.context, Graphics::ExportMappings(product->state), Graphics::PixelProgramSkipped(queue)); Graphics::NormalizeUnusedViewportDepth(product->state, product->pixel); Graphics::ApplyDepthRemap(context, product->state); + Graphics::ApplyDepthBoundsEmulation(context, product->state, product->pixel); product->pixel.targetExportPacking = Graphics::ExportPackings(product->state); product->pixel.dualSourceBlend = product->state.dualSourceBlend; product->pixel.reversedBlend = product->state.reversedBlend; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp index a1a5ebaab1..a34ffbd972 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp @@ -482,6 +482,10 @@ bool DrawPipeline::HeldReads(std::uint64_t address, std::size_t bytes) { return false; } +std::uint64_t DrawPipeline::HeldReadQueries() { + return heldReadQueries.load(std::memory_order_relaxed); +} + void DrawPipeline::ReleaseRunningReads() { std::lock_guard lock(mutex); if (!items.empty()) items.front().reads.clear(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp index af6202cb08..fbe526d30a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp @@ -19,9 +19,8 @@ void Submit(const Packet* packet, std::uint32_t queue) try { return; } GuestMemory::CheckRange(packet, sizeof(Packet), alignof(Packet)); - recorder->Submit(*packet, queue); const auto flips = driver.FlipSubmissions(); - driver.Submit(packet, queue); + recorder->Submit(*packet, queue, [&] { driver.Submit(packet, queue); }); if (driver.FlipSubmissions() != flips) recorder->FrameBoundary(); } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); @@ -41,8 +40,12 @@ void RegisterShader(const Shader* shader) { } void SuspendPoint() { - if (auto* const recorder = FrameRecorder::Active()) recorder->Suspend(); - DriverDetail::Driver::Get().SuspendPoint(); + auto* const recorder = FrameRecorder::Active(); + if (recorder == nullptr) { + DriverDetail::Driver::Get().SuspendPoint(); + return; + } + recorder->Suspend([] { DriverDetail::Driver::Get().SuspendPoint(); }); } void RegisterVideoOutput(std::uint32_t handle, const std::shared_ptr& output) { @@ -59,13 +62,13 @@ void AttachWindow(const PresentationWindow& window) { DriverDetail::Driver::Get().AttachWindow(window); } -void PresentClear(const PresentationWindow& window, bool opaque, void (*gpuReady)(void*), void* context) { - DriverDetail::Driver::Get().Present(window, nullptr, opaque, gpuReady, context); +void PresentClear(const PresentationWindow& window, bool opaque, const DisplayBuffer* plane, void (*gpuReady)(void*), void* context) { + DriverDetail::Driver::Get().Present(window, nullptr, plane, opaque, gpuReady, context); } -void PresentBuffer(const PresentationWindow& window, const DisplayBuffer& buffer, void (*gpuReady)(void*), void* context) { +void PresentBuffer(const PresentationWindow& window, const DisplayBuffer& buffer, const DisplayBuffer* plane, void (*gpuReady)(void*), void* context) { if (auto* const recorder = FrameRecorder::Active()) recorder->PresentBuffer(buffer); - DriverDetail::Driver::Get().Present(window, &buffer, true, gpuReady, context); + DriverDetail::Driver::Get().Present(window, &buffer, plane, true, gpuReady, context); } void ReleaseWindow(void* window) { @@ -146,9 +149,12 @@ extern "C" void AgcDriverSerialProgress_nid_postfix(std::uint64_t* accepted, std extern "C" void AgcDriverSubmitFlip_nid_postfix(const AgcDriver::FlipInfo& info) try { auto* const recorder = AgcDriver::FrameRecorder::Active(); - if (recorder != nullptr) recorder->Flip(info); - AgcDriver::DriverDetail::Driver::Get().SubmitFlip(info); - if (recorder != nullptr) recorder->FrameBoundary(); + if (recorder == nullptr) { + AgcDriver::DriverDetail::Driver::Get().SubmitFlip(info); + return; + } + recorder->Flip(info, [&] { AgcDriver::DriverDetail::Driver::Get().SubmitFlip(info); }); + recorder->FrameBoundary(); } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); } @@ -157,6 +163,14 @@ extern "C" std::uint64_t AgcDriverDispatchesAdoptedAhead_nid_postfix() { return AgcDriver::DriverDetail::Driver::Get().DispatchesAdoptedAhead(); } +extern "C" std::uint64_t AgcDriverDispatchesRejectedAhead_nid_postfix() { + return AgcDriver::DriverDetail::Driver::Get().DispatchesRejectedAhead(); +} + +extern "C" void AgcDriverSetAheadAdoptHook_nid_postfix(void (*hook)()) { + AgcDriver::DriverDetail::Driver::Get().SetAheadAdoptHook(hook); +} + extern "C" void AgcDriverRegisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr& output) { AgcDriver::RegisterVideoOutput(handle, output); } @@ -169,12 +183,12 @@ extern "C" void AgcDriverAttachWindow_nid_postfix(const AgcDriver::PresentationW AgcDriver::AttachWindow(window); } -extern "C" void AgcDriverPresentClear_nid_postfix(const AgcDriver::PresentationWindow& window, bool opaque, void (*gpuReady)(void*), void* context) { - AgcDriver::PresentClear(window, opaque, gpuReady, context); +extern "C" void AgcDriverPresentClear_nid_postfix(const AgcDriver::PresentationWindow& window, bool opaque, const AgcDriver::DisplayBuffer* plane, void (*gpuReady)(void*), void* context) { + AgcDriver::PresentClear(window, opaque, plane, gpuReady, context); } -extern "C" void AgcDriverPresentBuffer_nid_postfix(const AgcDriver::PresentationWindow& window, const AgcDriver::DisplayBuffer& buffer, void (*gpuReady)(void*), void* context) { - AgcDriver::PresentBuffer(window, buffer, gpuReady, context); +extern "C" void AgcDriverPresentBuffer_nid_postfix(const AgcDriver::PresentationWindow& window, const AgcDriver::DisplayBuffer& buffer, const AgcDriver::DisplayBuffer* plane, void (*gpuReady)(void*), void* context) { + AgcDriver::PresentBuffer(window, buffer, plane, gpuReady, context); } extern "C" void AgcDriverReleaseWindow_nid_postfix(void* window) { 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 6328766099..7e0d9e571c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -11,6 +11,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Eq/include/Event.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include #include #include @@ -29,11 +30,6 @@ bool orderedFills() { return ordered; } -bool pipelineDma() { - static const bool enabled = std::getenv("APS5_PIPELINE_DMA") != nullptr; - return enabled; -} - bool keepsDrawFinisher(std::uint32_t header, std::uint32_t opcode) { if (header == FlipPacketHeader || header == RenderingWaitPacketHeader) return false; if (Pm4::DrawOpcode(opcode)) return true; @@ -65,7 +61,6 @@ bool keepsDrawPipeline(std::span packet, std::uint32_t head if (packet.size() < 7) return false; const auto destination = Pm4::DmaDestination(packet); if (destination == Pm4::DmaDestinationNowhere) return true; - if (!pipelineDma()) return false; if (destination == Pm4::DmaSelectGds) return true; const auto bytes = Pm4::DmaBytes(packet); if (bytes == 0) return true; @@ -76,8 +71,8 @@ bool keepsDrawPipeline(std::span packet, std::uint32_t head } case 0x3c: case 0x93: { if (packet.size() < 5) return false; - const std::uint64_t awaited = packet[2] | (static_cast(packet[3]) << 32u); const auto bytes = Pm4::WaitAwaitedBytes(packet); + const std::uint64_t awaited = (packet[2] & (bytes == 8 ? ~7u : ~3u)) | (static_cast(packet[3]) << 32u); if (DrawPipeline::Queue0().Overlaps(awaited, bytes)) return false; GuestMemory::CheckRange(reinterpret_cast(awaited), bytes, bytes); return Pm4::WaitSatisfiedUnchecked(packet); @@ -400,6 +395,7 @@ void Driver::execute(const Submission& submission) { GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); std::lock_guard gpuLock(GuestMemory::GpuMutex()); const auto localDevice = device.Load(); + if (localDevice != nullptr) static_cast(Graphics::StorageTexture::StoreDrawnLinear("flip")); if (!recordLabelsForPacket(localDevice.get(), submission.queue) && localDevice != nullptr) localDevice->SubmitRecorded(submission.queue == 0); if (localDevice != nullptr) { 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 5c2cc06454..2e9ce460eb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -38,11 +38,12 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue std::lock_guard gpuLock(GuestMemory::GpuMutex()); const auto localDevice = device.Load(); recordDeferredLabels(localDevice.get(), submission.queue); - const bool workOpen = Graphics::Recorder::RecordedWorkSinceSubmit() != 0; + bool workOpen = Graphics::Recorder::RecordedWorkSinceSubmit() != 0; int reason = localDevice != nullptr ? 0 : 1; if (label.has_value()) { const auto bytes = label->Bytes(); const auto stamp = ++eventSerial; + if (localDevice != nullptr && submission.queue == 0 && Graphics::StorageTexture::StoreDrawnLinear("label") != 0) workOpen = true; reason = localDevice != nullptr ? localDevice->WriteLabelOnGpu(label->address, bytes, stamp, submission.queue) : 4; if (reason == 1) GuestMemory::Write(label->address, bytes, 4); if (reason == 0 || reason == 1 || reason == 5) { @@ -52,6 +53,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue countLabelOutcome(reason); ++immediateLabels; } else { + if (localDevice != nullptr && submission.queue == 0 && Graphics::StorageTexture::StoreDrawnLinear("interrupt") != 0) workOpen = true; if (localDevice != nullptr) Graphics::StorageTexture::StoreReadbackBeforeLabel("readback before interrupt"); ++noOpLabels; } @@ -249,6 +251,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue recordDeferredLabels(draining.get(), submission.queue); if (draining != nullptr) { + static_cast(Graphics::StorageTexture::StoreDrawnLinear("drain")); if (unlockedDrain && draining->CanWaitUnlocked()) epoch = draining->SubmitAndEpoch(); else draining->WaitIdle(); } @@ -292,8 +295,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) { - static const bool enabled = std::getenv("APS5_PIPELINE_DMA") != nullptr; - if (!enabled || opcode != 0x50 || queue != 0 || packet.size() < 7 || !DrawPipeline::Active() || !deferredLabels().labels.empty()) return false; + 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); constexpr std::size_t immediateLimit = std::size_t{16} << 20u; constexpr std::size_t gpuStoreLimit = 65536; @@ -438,10 +440,11 @@ void Driver::commitLabel(std::uint32_t queue, std::uint64_t address, std::spanWriteLabelOnGpu(address, bytes, stamp, queue) : 4; if (reason != 0 && reason != 5 && reason != 6) { if (reason != 1 && localDevice != nullptr) localDevice->WaitIdle(); @@ -456,6 +459,7 @@ void Driver::commitLabel(std::uint32_t queue, std::uint64_t address, std::spanAfterRecordedWork([this, queue] { deliverGuestEopInterrupt(queue); }, queue == 0); if (deferred && workOpen) localDevice->SubmitRecorded(queue == 0); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp index a2fa486528..d607e1ae53 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp @@ -70,7 +70,7 @@ void Driver::AttachWindow(const PresentationWindow& window) { device = std::make_shared(&window); } -void Driver::Present(const PresentationWindow& window, const DisplayBuffer* buffer, bool opaque, void (*gpuReady)(void*), void* context) { +void Driver::Present(const PresentationWindow& window, const DisplayBuffer* buffer, const DisplayBuffer* plane, bool opaque, void (*gpuReady)(void*), void* context) { if (PerformanceTrace::Configured()) PerformanceTrace::Present(window.width, window.height, device.Load().get()); PerformanceContext timingContext(window.timing.get()); PerformanceTimer timing("Driver.Present"); @@ -93,6 +93,7 @@ void Driver::Present(const PresentationWindow& window, const DisplayBuffer* buff bool submitted = false; bool presentable = false; bool trailing = false; + bool queueUnlocked = false; double waitedMs = 0; { GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Present); @@ -129,10 +130,10 @@ void Driver::Present(const PresentationWindow& window, const DisplayBuffer* buff presenting->WaitIdle(); timing.Mark("device_idle_wait"); } - submitted = presenting->PresentDisplayBuffer(*buffer); + submitted = presenting->PresentDisplayBuffer(*buffer, plane); timing.Mark("present_display_buffer"); } else { - submitted = presenting->PresentClear(window.width, window.height, opaque); + submitted = presenting->PresentClear(window.width, window.height, opaque, plane); timing.Mark("present_clear"); } if (submitted && syncFlip) { @@ -141,12 +142,20 @@ void Driver::Present(const PresentationWindow& window, const DisplayBuffer* buff } if (submitted && (syncFlip || inFlight != 0)) { - presenting->QueuePresent(); - timing.Mark("queue_present"); + if (presenting->PresentsOnOwnQueue()) { + queueUnlocked = true; + } else { + presenting->QueuePresent(); + timing.Mark("queue_present"); + } trailing = !syncFlip; submitted = false; } } + if (queueUnlocked) { + presenting->QueuePresent(); + timing.Mark("queue_present"); + } if (trailing) { waitedMs += presenting->FinishPresent(); timing.Mark("render_fence_wait"); @@ -155,9 +164,13 @@ void Driver::Present(const PresentationWindow& window, const DisplayBuffer* buff waitedMs = presenting->FinishPresent(); timing.Mark("render_fence_wait"); - GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Present); - std::lock_guard lock(GuestMemory::GpuMutex()); - presenting->QueuePresent(); + if (presenting->PresentsOnOwnQueue()) { + presenting->QueuePresent(); + } else { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Present); + std::lock_guard lock(GuestMemory::GpuMutex()); + presenting->QueuePresent(); + } timing.Mark("queue_present"); } gpuReady(context); @@ -193,7 +206,7 @@ void Driver::reportPresents(double waitedMs, std::size_t inFlight) { const auto flips = flipsCounted.load() - previousFlips; const double perFlip = flips != 0 ? 1.0 / static_cast(flips) : 0.0; const auto serial = flipSerial.load(); - AgcDriver::ProfilePrint_nid_no_patch( "[present] %llu presents over 10 s (%llu dropped: no free swapchain image; %zu may trail on the GPU); %s p50 %.2f ms, p90 %.2f ms, max %.2f ms over %zu; per frame, presented or dropped: GPU busy %.1f ms, idle gaps %.1f ms (%llu batches without a completion record); per flip: %.1f batches recorded (%.1f unsignaled at the flip); per present: %.1f batches ahead of the blit, %.1f after it; after the flip packet: last submit %.1f ms, blit submit %.1f ms; in-place reads overwritten before execution: %llu of %llu checked\n", static_cast(presents), static_cast(dropped), inFlight, inFlight != 0 ? "inflight_wait" : "render_fence_wait", percentile(0.5), percentile(0.9), waits.empty() ? 0.0 : waits.back(), waits.size(), (counts.gpuBusyMs - previous.gpuBusyMs) * perFrame, (counts.gpuGapMs - previous.gpuGapMs) * perFrame, static_cast(counts.gpuUnread - previous.gpuUnread), static_cast(serial - windowSerial) * perFlip, static_cast(flipBatchesUnsignaled.load() - previousUnsignaled) * perFlip, static_cast(counts.batchesAheadOfBlit - previous.batchesAheadOfBlit) * perPresent, static_cast(counts.batchesAfterFlip - previous.batchesAfterFlip) * perPresent, (counts.lastSubmitAfterFlipMs - previous.lastSubmitAfterFlipMs) * perPresent, (counts.blitSubmitAfterFlipMs - previous.blitSubmitAfterFlipMs) * perPresent, static_cast(counts.readsOverwritten - previous.readsOverwritten), static_cast(counts.readsChecked - previous.readsChecked)); + AgcDriver::ProfilePrint_nid_no_patch( "[present] %llu presents over 10 s (%llu dropped: no free swapchain image; %zu may trail on the GPU); %s p50 %.2f ms, p90 %.2f ms, max %.2f ms over %zu; per frame, presented or dropped: GPU busy %.1f ms, idle gaps %.1f ms (%llu batches without a completion record); per flip: %.1f batches recorded (%.1f unsignaled at the flip); per present: %.1f batches ahead of the blit, %.1f after it; after the flip packet: last submit %.1f ms, blit submit %.1f ms; in-place reads overwritten before execution: %llu of %llu checked; waits for the display keys' writer outside the GPU mutex: %llu, %.1f ms\n", static_cast(presents), static_cast(dropped), inFlight, inFlight != 0 ? "inflight_wait" : "render_fence_wait", percentile(0.5), percentile(0.9), waits.empty() ? 0.0 : waits.back(), waits.size(), (counts.gpuBusyMs - previous.gpuBusyMs) * perFrame, (counts.gpuGapMs - previous.gpuGapMs) * perFrame, static_cast(counts.gpuUnread - previous.gpuUnread), static_cast(serial - windowSerial) * perFlip, static_cast(flipBatchesUnsignaled.load() - previousUnsignaled) * perFlip, static_cast(counts.batchesAheadOfBlit - previous.batchesAheadOfBlit) * perPresent, static_cast(counts.batchesAfterFlip - previous.batchesAfterFlip) * perPresent, (counts.lastSubmitAfterFlipMs - previous.lastSubmitAfterFlipMs) * perPresent, (counts.blitSubmitAfterFlipMs - previous.blitSubmitAfterFlipMs) * perPresent, static_cast(counts.readsOverwritten - previous.readsOverwritten), static_cast(counts.readsChecked - previous.readsChecked), static_cast(counts.keyWriterWaits - previous.keyWriterWaits), counts.keyWriterWaitMs - previous.keyWriterWaitMs); previous = counts; previousFlips = flipsCounted.load(); previousUnsignaled = flipBatchesUnsignaled.load(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index cccf175abf..312b71958b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -138,7 +138,7 @@ std::shared_ptr ReadRawShader(std::uint64_t address) { cache.erase(found); } const auto bytes = result->code.size() * sizeof(std::uint32_t); - while (!cache.empty() && (cache.size() >= 64 || cacheBytes + bytes > 8 * 1024 * 1024)) { + while (!cache.empty() && (cache.size() >= 4096 || cacheBytes + bytes > 64 * 1024 * 1024)) { cacheBytes -= cache.back()->code.size() * sizeof(std::uint32_t); cache.pop_back(); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp index 8d34708b90..cdfdc9afbe 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp @@ -163,20 +163,20 @@ void FrameRecorder::UnregisterVideoOutput(std::uint32_t handle) { record(std::move(call), true); } -void FrameRecorder::Submit(const Packet& packet, std::uint32_t queue) { +void FrameRecorder::Submit(const Packet& packet, std::uint32_t queue, const std::function& accept) { auto call = make(FrameCallKind::Submit); call.queue = queue; call.address = pointer(packet.addr); call.size = packet.dw_num; call.flags = packet.flags; - record(std::move(call), false); + record(std::move(call), false, {}, accept); } -void FrameRecorder::Flip(const FlipInfo& info) { +void FrameRecorder::Flip(const FlipInfo& info, const std::function& accept) { auto call = make(FrameCallKind::Flip); call.cpuFlip = true; call.values = {info.handle, flipValue(info.index), info.mode, flipValue(info.argument)}; - record(std::move(call), false); + record(std::move(call), false, {}, accept); } void FrameRecorder::ReserveEopFlip(const FlipInfo& info, std::uint32_t token) { @@ -198,8 +198,8 @@ void FrameRecorder::ReserveEopFlip(const FlipInfo& info, std::uint32_t token) { ++recordedCalls; } -void FrameRecorder::Suspend() { - record(make(FrameCallKind::Suspend), false); +void FrameRecorder::Suspend(const std::function& accept) { + record(make(FrameCallKind::Suspend), false, {}, accept); } void FrameRecorder::PresentBuffer(const DisplayBuffer& buffer) { @@ -236,8 +236,13 @@ void FrameRecorder::writeReferenceFrames() { } } -void FrameRecorder::record(FrameCall call, bool prologueCall, std::span shaders) { +void FrameRecorder::record(FrameCall call, bool prologueCall, std::span shaders, const std::function& accept) { std::lock_guard lock(mutex); + recordLocked(std::move(call), prologueCall, shaders); + if (accept) accept(); +} + +void FrameRecorder::recordLocked(FrameCall call, bool prologueCall, std::span shaders) { if (state == State::Waiting) { if (!prologueCall) return; call.ordinal = ++ordinal; @@ -360,10 +365,23 @@ void FrameRecorder::emitShaderDelta(std::span shaders) { void FrameRecorder::writeDelta(std::span spans, std::span hostPages, std::span added) { if (abandonIfDiskLow()) return; - const auto [accepted, completed] = DriverDetail::Driver::Get().SerialProgress(); + std::vector> blocks; + std::vector> written; + for (const auto& span : spans) { + GuestMemory::CollectWrittenBlocks(span.address, static_cast(span.bytes), span.since, written); + blocks.insert(blocks.end(), written.begin(), written.end()); + } + auto& driver = DriverDetail::Driver::Get(); + auto [accepted, completed] = driver.SerialProgress(); + if (completed < accepted && (!added.empty() || !blocks.empty())) { + if (!driver.WaitIdleFor(std::chrono::seconds(10))) { + stop("stopped: the GPU work submitted before a memory delta did not finish within 10 s"); + return; + } + std::tie(accepted, completed) = driver.SerialProgress(); + } const auto group = deltaGroups + 1; bool any = false; - std::vector> written; std::vector bytes; for (const auto& range : added) { writer->WriteRange(range.address, range.bytes, range.kind, range.protection, range.access); @@ -397,15 +415,12 @@ void FrameRecorder::writeDelta(std::span spans, std::span(span.bytes), span.since, written); - for (const auto& [begin, end] : written) { - bytes.resize(static_cast(end - begin)); - GuestMemory::ReadCommittedUnsynced(begin, bytes); - writer->WriteDelta(group, completed, begin, bytes); - deltaBytes += bytes.size(); - any = true; - } + for (const auto& [begin, end] : blocks) { + bytes.resize(static_cast(end - begin)); + GuestMemory::ReadCommittedUnsynced(begin, bytes); + writer->WriteDelta(group, completed, begin, bytes); + deltaBytes += bytes.size(); + any = true; } if (!any) return; deltaGroups = group; @@ -436,8 +451,13 @@ bool FrameRecorder::abandonIfDiskLow() { std::error_code error; const auto space = std::filesystem::space(directory, error); if (error || space.available >= MinimumFreeBytes) return false; - warnings.push_back("stopped: less than 4 GiB free on the capture volume"); - std::fprintf(stderr, "[frame-capture] stopped: less than 4 GiB free on the capture volume\n"); + stop("stopped: less than 4 GiB free on the capture volume"); + return true; +} + +void FrameRecorder::stop(const char* reason) { + warnings.push_back(reason); + std::fprintf(stderr, "[frame-capture] %s\n", reason); try { writer->Finish(header()); } catch (const std::exception& failure) { @@ -445,7 +465,6 @@ bool FrameRecorder::abandonIfDiskLow() { } writer.reset(); state = State::Done; - return true; } void FrameRecorder::start() { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index fe8241fcfc..e72fabda25 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -1751,7 +1752,7 @@ void Rewatch(std::span> ranges) { if (!tracker.watched) return; bool restored = false; for (const auto& [begin, end] : ranges) { - if (end <= begin || begin < tracker.base || end - tracker.base > tracker.size || (begin - tracker.base) % WriteBlockBytes != 0 || (end - tracker.base) % WriteBlockBytes != 0) continue; + if (end <= begin || begin < tracker.base || end - tracker.base > tracker.size || (begin - tracker.base) % WritePageBytes != 0 || (end - tracker.base) % WritePageBytes != 0) continue; const auto bytes = static_cast(end - begin); if (tracker.coverage.Covers(begin, bytes)) continue; GuestArena::GuestArenaCompareWhenShared_nid_no_patch(static_cast(begin), bytes); @@ -1770,7 +1771,7 @@ void Rewatch(std::span> ranges) { #endif } -bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function& import, bool staysWatched) { +bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function& import, bool staysWatched, bool importMayWriteGuest) { if (bytes == 0 || bytes > std::numeric_limits::max() - address) return import(); if (!staysWatched) { constexpr std::uint64_t page = 4096; @@ -1790,7 +1791,11 @@ bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function const bool covered = tracker.covers(first, static_cast(stop - first)); const auto runImport = [&] { #ifdef __APPLE__ - const GuestArena::HostWrite open(reinterpret_cast(first), static_cast(stop - first)); + std::optional open; + if (importMayWriteGuest) open.emplace(reinterpret_cast(first), static_cast(stop - first)); +#endif +#ifndef __APPLE__ + static_cast(importMayWriteGuest); #endif return import(); }; @@ -1820,12 +1825,26 @@ bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function hashes[static_cast((cursor - first) / page)] = HashPage(reinterpret_cast(cursor)); } } - std::erase_if(spans, [&](const auto& span) { return !walkWrites(tracker, span.first, span.second, StampKind::Cpu, true); }); + const auto saveBytes = [&](std::uint64_t begin, std::uint64_t end) { + lock.Suspend(); + beforeUnwatch(begin, static_cast(end - begin)); + lock.Resume(); + }; + std::vector> leaving; + std::erase_if(spans, [&](const auto& span) { + if (walkWrites(tracker, span.first, span.second, StampKind::Cpu, true)) return false; + leaving.push_back(span); + return true; + }); + for (const auto& [begin, end] : leaving) saveBytes(begin, end); if (!runImport()) return false; const WindowCopy window{first, hashes}; auto unwatchedFrom = first; for (const auto& [begin, end] : spans) { - if (!walkWrites(tracker, begin, end, StampKind::Cpu, true, &window)) continue; + if (!walkWrites(tracker, begin, end, StampKind::Cpu, true, &window)) { + saveBytes(begin, end); + continue; + } #ifdef _WIN32 GuestArena::GuestArenaCompareResidentWhenShared_nid_no_patch(static_cast(begin), static_cast(end - begin)); #endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp index e42e4e2b03..c1d4a777f5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp @@ -86,6 +86,7 @@ void PresentationPass::release() noexcept { for (std::size_t i = 0; i < slots.size(); ++i) { Release(i); releaseDumpTarget(slots[i]); + releaseUploadImage(slots[i]); } for (const auto& [key, entry] : targets) { if (entry.pipeline) context.Function("vkDestroyPipeline")(context.device, entry.pipeline, nullptr); @@ -155,22 +156,24 @@ VkFramebuffer PresentationPass::createFramebuffer(VkRenderPass renderPass, VkIma return framebuffer; } -const PresentationPass::Target& PresentationPass::target(VkFormat format, VkImageLayout finalLayout) { - const auto key = std::pair{format, finalLayout}; +const PresentationPass::Target& PresentationPass::target(VkFormat format, VkImageLayout finalLayout, VkImageLayout loadFrom) { + const auto key = std::tuple{format, finalLayout, loadFrom}; if (const auto found = targets.find(key); found != targets.end()) return found->second; + const bool blended = loadFrom != VK_IMAGE_LAYOUT_UNDEFINED; VkFormatProperties properties{}; context.formatProperties(context.physical, format, &properties); Graphics::Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0, "presentation pass format is not renderable"); + Graphics::Require(!blended || (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT) != 0, "presentation pass cannot blend an overlay into this format"); Target created; try { VkAttachmentDescription attachment{}; attachment.format = format; attachment.samples = VK_SAMPLE_COUNT_1_BIT; - attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.loadOp = blended ? VK_ATTACHMENT_LOAD_OP_LOAD : VK_ATTACHMENT_LOAD_OP_DONT_CARE; attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachment.initialLayout = loadFrom; attachment.finalLayout = finalLayout; const VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; VkSubpassDescription subpass{}; @@ -179,7 +182,7 @@ const PresentationPass::Target& PresentationPass::target(VkFormat format, VkImag subpass.pColorAttachments = &reference; const bool transfer = finalLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; const VkSubpassDependency dependencies[2] = { - {VK_SUBPASS_EXTERNAL, 0, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, 0}, + {VK_SUBPASS_EXTERNAL, 0, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, blended ? VkAccessFlags{VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT} : VkAccessFlags{0}, blended ? VkAccessFlags{VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT} : VkAccessFlags{VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT}, 0}, {0, VK_SUBPASS_EXTERNAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, transfer ? VK_PIPELINE_STAGE_TRANSFER_BIT : VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, transfer ? VK_ACCESS_TRANSFER_READ_BIT : VkAccessFlags{0}, 0}, }; VkRenderPassCreateInfo renderPassInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; @@ -212,7 +215,16 @@ const PresentationPass::Target& PresentationPass::target(VkFormat format, VkImag VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; VkPipelineColorBlendAttachmentState blendAttachment{}; - blendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + blendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | (blended ? 0u : VK_COLOR_COMPONENT_A_BIT); + if (blended) { + blendAttachment.blendEnable = VK_TRUE; + blendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; + blendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + blendAttachment.colorBlendOp = VK_BLEND_OP_ADD; + blendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; + blendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + blendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; + } VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; blend.attachmentCount = 1; blend.pAttachments = &blendAttachment; @@ -262,7 +274,75 @@ void PresentationPass::Bind(std::size_t index, VkImage image, std::uint32_t widt context.Function("vkUpdateDescriptorSets")(context.device, 1, &write, 0, nullptr); } -void PresentationPass::draw(VkCommandBuffer commands, const Slot& slot, const Target& entry, VkFramebuffer framebuffer, VkExtent2D extent, const std::int32_t (¶meters)[9]) const { +void PresentationPass::releaseUploadImage(Slot& slot) noexcept { + if (slot.uploadImage) context.Function("vkDestroyImage")(context.device, slot.uploadImage, nullptr); + if (slot.uploadMemory) context.Function("vkFreeMemory")(context.device, slot.uploadMemory, nullptr); + slot.uploadImage = VK_NULL_HANDLE; + slot.uploadMemory = VK_NULL_HANDLE; + slot.uploadExtent = {}; + slot.uploadTexels.reset(); +} + +void PresentationPass::ensureUploadImage(Slot& slot, VkExtent2D extent) { + if (slot.uploadImage != VK_NULL_HANDLE && slot.uploadExtent.width == extent.width && slot.uploadExtent.height == extent.height) return; + releaseUploadImage(slot); + try { + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = VK_FORMAT_R32_UINT; + imageInfo.extent = {extent.width, extent.height, 1u}; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Graphics::Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &slot.uploadImage), "vkCreateImage presentation upload"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, slot.uploadImage, &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); + Graphics::Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &slot.uploadMemory), "vkAllocateMemory presentation upload"); + Graphics::Check(context.Function("vkBindImageMemory")(context.device, slot.uploadImage, slot.uploadMemory, 0), "vkBindImageMemory presentation upload"); + slot.uploadTexels = std::make_unique(context, static_cast(extent.width) * extent.height * 4, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + slot.uploadExtent = extent; + } catch (...) { + releaseUploadImage(slot); + throw; + } +} + +void PresentationPass::BindTexels(std::size_t index, VkCommandBuffer commands, std::span texels, std::uint32_t width, std::uint32_t height, std::uint64_t pixelFormat, float whiteNits) { + Graphics::Require(index < slots.size(), "presentation pass slot is out of range"); + Graphics::Require(width != 0 && height != 0 && texels.size() == static_cast(width) * height * 4, "presentation pass texels do not cover their extent"); + auto& slot = slots[index]; + ensureUploadImage(slot, {width, height}); + std::memcpy(slot.uploadTexels->Bytes().data(), texels.data(), texels.size()); + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = slot.uploadImage; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const auto pipelineBarrier = context.Function("vkCmdPipelineBarrier"); + pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {width, height, 1u}; + context.Function("vkCmdCopyBufferToImage")(commands, slot.uploadTexels->Handle(), slot.uploadImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; + pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + Bind(index, slot.uploadImage, width, height, pixelFormat, whiteNits); +} + +void PresentationPass::draw(VkCommandBuffer commands, const Slot& slot, const Target& entry, VkFramebuffer framebuffer, VkExtent2D extent, VkRect2D scissor, const std::int32_t (¶meters)[9]) const { VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; begin.renderPass = entry.renderPass; begin.framebuffer = framebuffer; @@ -270,7 +350,6 @@ void PresentationPass::draw(VkCommandBuffer commands, const Slot& slot, const Ta context.Function("vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, entry.pipeline); const VkViewport viewport{0.0f, 0.0f, static_cast(extent.width), static_cast(extent.height), 0.0f, 1.0f}; - const VkRect2D scissor{{0, 0}, extent}; context.Function("vkCmdSetViewport")(commands, 0, 1, &viewport); context.Function("vkCmdSetScissor")(commands, 0, 1, &scissor); context.Function("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &slot.set, 0, nullptr); @@ -279,12 +358,13 @@ void PresentationPass::draw(VkCommandBuffer commands, const Slot& slot, const Ta context.Function("vkCmdEndRenderPass")(commands); } -void PresentationPass::RecordPresent(VkCommandBuffer commands, std::size_t index, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout finalLayout) { - Graphics::Require(index < slots.size() && slots[index].source != VK_NULL_HANDLE, "presentation pass slot has no source"); - Graphics::Require(destination != VK_NULL_HANDLE && extent.width != 0 && extent.height != 0, "presentation pass destination is unavailable"); - auto& slot = slots[index]; - const auto& entry = target(format, finalLayout); - const auto view = createView(destination, format, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | (finalLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0u)); +VkFramebuffer PresentationPass::destinationFramebuffer(Slot& slot, const Target& entry, VkImage destination, VkFormat format, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout) { + VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + for (const auto layout : {initialLayout, finalLayout}) { + if (layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + if (layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; + } + const auto view = createView(destination, format, usage); VkFramebuffer framebuffer = VK_NULL_HANDLE; try { framebuffer = createFramebuffer(entry.renderPass, view, extent); @@ -293,12 +373,35 @@ void PresentationPass::RecordPresent(VkCommandBuffer commands, std::size_t index throw; } slot.transient.emplace_back(view, framebuffer); + return framebuffer; +} + +void PresentationPass::RecordPresent(VkCommandBuffer commands, std::size_t index, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout finalLayout) { + Graphics::Require(index < slots.size() && slots[index].source != VK_NULL_HANDLE, "presentation pass slot has no source"); + Graphics::Require(destination != VK_NULL_HANDLE && extent.width != 0 && extent.height != 0, "presentation pass destination is unavailable"); + auto& slot = slots[index]; + const auto& entry = target(format, finalLayout); + const auto framebuffer = destinationFramebuffer(slot, entry, destination, format, extent, VK_IMAGE_LAYOUT_UNDEFINED, finalLayout); const auto rect = ComputeContainRect_nid_postfix(slot.width, slot.height, extent.width, extent.height); const bool hdr10 = colorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT; Graphics::Require(colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR || (hdr10 && (slot.mode & ModePq) != 0 && TenBitFormat(format)), "presentation pass presents only PQ display buffers to a 10-bit HDR10 destination"); const std::uint32_t mode = (hdr10 ? slot.mode & ~ModePq : slot.mode) | (TenBitFormat(format) ? ModeKeepTenBit : 0u); const std::int32_t parameters[9] = {rect.x, rect.y, static_cast(rect.width), static_cast(rect.height), static_cast(slot.width), static_cast(slot.height), 1, static_cast(mode), std::bit_cast(slot.whiteNits)}; - draw(commands, slot, entry, framebuffer, extent, parameters); + draw(commands, slot, entry, framebuffer, extent, {{0, 0}, extent}, parameters); +} + +void PresentationPass::RecordOverlay(VkCommandBuffer commands, std::size_t index, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout) { + Graphics::Require(index < slots.size() && slots[index].source != VK_NULL_HANDLE, "presentation pass slot has no source"); + Graphics::Require(destination != VK_NULL_HANDLE && extent.width != 0 && extent.height != 0, "presentation pass destination is unavailable"); + Graphics::Require(initialLayout != VK_IMAGE_LAYOUT_UNDEFINED, "an overlay is blended over a destination whose contents are kept"); + auto& slot = slots[index]; + Graphics::Require((slot.mode & (ModeTenBit | ModePq)) == 0, "an overlay plane is blended only from an 8-bit SDR display buffer"); + Graphics::Require(colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, "an overlay plane is blended only into an SDR destination"); + const auto& entry = target(format, finalLayout, initialLayout); + const auto framebuffer = destinationFramebuffer(slot, entry, destination, format, extent, initialLayout, finalLayout); + const auto rect = ComputeContainRect_nid_postfix(slot.width, slot.height, extent.width, extent.height); + const std::int32_t parameters[9] = {rect.x, rect.y, static_cast(rect.width), static_cast(rect.height), static_cast(slot.width), static_cast(slot.height), 1, static_cast(slot.mode), std::bit_cast(slot.whiteNits)}; + draw(commands, slot, entry, framebuffer, extent, {{rect.x, rect.y}, {rect.width, rect.height}}, parameters); } void PresentationPass::releaseDumpTarget(Slot& slot) noexcept { @@ -346,18 +449,38 @@ void PresentationPass::ensureDumpTarget(Slot& slot, VkExtent2D extent) { } void PresentationPass::RecordDump(VkCommandBuffer commands, std::size_t index, std::uint32_t stride, VkBuffer destination) { + RecordDumpImage(commands, index, stride); + RecordDumpReadback(commands, index, destination); +} + +void PresentationPass::RecordDumpImage(VkCommandBuffer commands, std::size_t index, std::uint32_t stride) { Graphics::Require(index < slots.size() && slots[index].source != VK_NULL_HANDLE, "presentation pass slot has no source"); - Graphics::Require(destination != VK_NULL_HANDLE, "presentation dump buffer is unavailable"); auto& slot = slots[index]; const auto extent = DumpExtent(slot.width, slot.height, stride); ensureDumpTarget(slot, extent); const auto& entry = target(VK_FORMAT_B8G8R8A8_UNORM, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); const std::int32_t parameters[9] = {0, 0, static_cast(extent.width), static_cast(extent.height), static_cast(slot.width), static_cast(slot.height), static_cast(stride), static_cast(slot.mode | ModeSubsample), std::bit_cast(slot.whiteNits)}; - draw(commands, slot, entry, slot.dumpFramebuffer, extent, parameters); + draw(commands, slot, entry, slot.dumpFramebuffer, extent, {{0, 0}, extent}, parameters); +} + +VkImage PresentationPass::DumpImage(std::size_t index) const { + Graphics::Require(index < slots.size() && slots[index].dumpImage != VK_NULL_HANDLE, "presentation pass slot has no dump image"); + return slots[index].dumpImage; +} + +VkExtent2D PresentationPass::DumpImageExtent(std::size_t index) const { + Graphics::Require(index < slots.size() && slots[index].dumpImage != VK_NULL_HANDLE, "presentation pass slot has no dump image"); + return slots[index].dumpExtent; +} + +void PresentationPass::RecordDumpReadback(VkCommandBuffer commands, std::size_t index, VkBuffer destination) { + Graphics::Require(destination != VK_NULL_HANDLE, "presentation dump buffer is unavailable"); + const auto image = DumpImage(index); + const auto extent = DumpImageExtent(index); VkBufferImageCopy copy{}; copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; copy.imageExtent = {extent.width, extent.height, 1u}; - context.Function("vkCmdCopyImageToBuffer")(commands, slot.dumpImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination, 1, ©); + context.Function("vkCmdCopyImageToBuffer")(commands, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination, 1, ©); Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp index d7c88d7b7d..54b8adae5f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" #include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" namespace AgcDriver { @@ -23,7 +24,7 @@ PresentationScaler::~PresentationScaler() { void PresentationScaler::release() noexcept { if (sourceImage != VK_NULL_HANDLE) context.Function("vkDestroyImage")(context.device, sourceImage, nullptr); - if (sourceMemory != VK_NULL_HANDLE) context.Function("vkFreeMemory")(context.device, sourceMemory, nullptr); + if (sourceMemory != VK_NULL_HANDLE) Graphics::ReleaseImageMemory(context, sourceMemory, sourceBytes, sourceMemoryType); sourceImage = VK_NULL_HANDLE; sourceMemory = VK_NULL_HANDLE; sourceWidth = 0; @@ -42,7 +43,9 @@ void PresentationScaler::EnsureSourceImage(std::uint32_t width, std::uint32_t he imageInfo.arrayLayers = 1; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; - imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + VkFormatProperties properties{}; + context.formatProperties(context.physical, sourceFormat, &properties); + imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | ((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT) != 0 ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0u); imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; Graphics::Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &sourceImage), "vkCreateImage presentation source"); @@ -50,8 +53,9 @@ void PresentationScaler::EnsureSourceImage(std::uint32_t width, std::uint32_t he context.Function("vkGetImageMemoryRequirements")(context.device, sourceImage, &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); - Graphics::Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &sourceMemory), "vkAllocateMemory presentation source"); + allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "presentation source"); + sourceMemory = Graphics::AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory presentation source", &sourceMemoryType, true); + sourceBytes = requirements.size; Graphics::Check(context.Function("vkBindImageMemory")(context.device, sourceImage, sourceMemory, 0), "vkBindImageMemory presentation source"); sourceWidth = width; sourceHeight = height; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp index 2db74fc43c..e88d6475f4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp @@ -12,6 +12,12 @@ #ifdef _WIN32 #include +#elif defined(__APPLE__) +#include +#include +#include +#include +#include #else #include #include @@ -42,7 +48,30 @@ void LogOnce(const char* role, const std::string& text) { std::fprintf(stderr, "[thread-priority] %s: %s\n", role, text.c_str()); } -#ifndef _WIN32 +#if defined(__APPLE__) + +void Raise(const char* role) { + if (RequestedMode() == ThreadPriorityMode::Realtime) { + mach_timebase_info_data_t timebase{}; + mach_timebase_info(&timebase); + const auto ticks = [&](std::uint64_t nanos) { + return static_cast(nanos * timebase.denom / timebase.numer); + }; + thread_time_constraint_policy_data_t policy{ticks(1000000), ticks(500000), ticks(1000000), TRUE}; + const auto status = thread_policy_set(pthread_mach_thread_np(pthread_self()), THREAD_TIME_CONSTRAINT_POLICY, + reinterpret_cast(&policy), THREAD_TIME_CONSTRAINT_POLICY_COUNT); + if (status != KERN_SUCCESS) + throw std::runtime_error(std::string("APS5_THREAD_PRIORITY=rt: ") + role + " time-constraint policy refused (kern_return " + std::to_string(status) + ")"); + LogOnce(role, "Mach time-constraint policy"); + return; + } + const int error = pthread_set_qos_class_self_np(QOS_CLASS_USER_INTERACTIVE, 0); + if (error != 0) + throw std::runtime_error(std::string("APS5_THREAD_PRIORITY=high: ") + role + " user-interactive QoS refused: " + std::strerror(error)); + LogOnce(role, "user-interactive QoS"); +} + +#elif !defined(_WIN32) struct DBusConnection; struct DBusMessage; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 70fdef29e3..31b43695a5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -8,6 +8,7 @@ #include "SpirvBackend/SpirvOptimizer.hpp" #endif #include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DeferredDestruction.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Execution/include/FloatFlushProbe.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" @@ -20,9 +21,11 @@ #include "prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp" #include "prx/libSceAgcDriver/Execution/include/DrawInputsInPlace.hpp" #include "prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp" +#include "prx/libSceAgcDriver/Execution/include/TessellationPipelineCache.hpp" #include "prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentMode.hpp" +#include "prx/libSceAgcDriver/Execution/include/SwapchainImages.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" @@ -83,7 +86,7 @@ namespace AgcDriver { -DeviceQueuePlan PlanDeviceQueues(std::span families, std::uint32_t family, bool settles, bool sparseBinding) { +DeviceQueuePlan PlanDeviceQueues(std::span families, std::uint32_t family, bool settles, bool sparseBinding, bool presents) { DeviceQueuePlan plan; const auto count = static_cast(families.size()); if (settles) { @@ -102,6 +105,13 @@ DeviceQueuePlan PlanDeviceQueues(std::span famili } } } + if (presents && family < count) { + if (const auto index = SelectPresentQueue(families[family].queueCount, plan.mainCount)) { + plan.present = true; + plan.presentIndex = *index; + plan.mainCount = *index + 1; + } + } if (!sparseBinding) return plan; const auto taken = [&](std::uint32_t index) { return plan.settle && index == plan.settleFamily ? plan.settleIndex + 1 : 0u; }; const auto binds = [&](std::uint32_t index) { return index != family && families[index].queueCount > taken(index) && (families[index].queueFlags & VK_QUEUE_SPARSE_BINDING_BIT) != 0; }; @@ -351,6 +361,7 @@ struct VulkanDevice::State { VkDriverId driverId = VK_DRIVER_ID_MAX_ENUM; std::uint32_t pixelFunctionStorageDwords = 4096u; bool individuallyFreeDescriptorSets = false; + bool tessellationPipelineCache = true; PFN_vkGetCalibratedTimestampsKHR calibratedTimestamps = nullptr; PFN_vkCmdSetCheckpointNV checkpointMarker = nullptr; PFN_vkCmdWriteBufferMarkerAMD bufferMarker = nullptr; @@ -359,6 +370,8 @@ struct VulkanDevice::State { void* markerMapping = nullptr; BufferMarkerLog markerLog; bool deviceLostHook = false; + bool deferDestruction = false; + PFN_vkGetDeviceProcAddr contextDeviceProc = nullptr; VkTimeDomainKHR hostTimeDomain = VK_TIME_DOMAIN_DEVICE_KHR; VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; std::vector capabilities{1}; @@ -439,6 +452,8 @@ struct VulkanDevice::State { std::uint32_t settleFamily = 0; VkCommandPool settlePool = VK_NULL_HANDLE; std::mutex settleQueueMutex; + VkQueue presentQueue = VK_NULL_HANDLE; + std::mutex presentQueueMutex; bool memoryBudget = false; bool depthRangeUnrestricted = false; bool samplerAnisotropy = false; @@ -506,6 +521,7 @@ struct VulkanDevice::State { VkPhysicalDeviceMeshShaderPropertiesEXT meshLimits{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_PROPERTIES_EXT}; std::unique_ptr scaler; std::unique_ptr presentation; + std::unique_ptr planePass; std::unique_ptr overlay; VkFormat surfaceFormat = VK_FORMAT_B8G8R8A8_UNORM; VkColorSpaceKHR surfaceColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; @@ -528,6 +544,7 @@ struct VulkanDevice::State { VkFence fence = VK_NULL_HANDLE; VkCommandBuffer commands = VK_NULL_HANDLE; std::shared_ptr kept; + std::shared_ptr keptPlane; std::unique_ptr dumpBuffer; std::unique_ptr dumpScaler; std::uint32_t dumpWidth = 0; @@ -729,7 +746,9 @@ struct VulkanDevice::State { void RetireSlot(PresentSlot& slot) { slot.inFlight = false; if (presentation) presentation->Release(static_cast(&slot - presentSlots.data())); + if (planePass) planePass->Release(static_cast(&slot - presentSlots.data())); slot.kept.reset(); + slot.keptPlane.reset(); if (slot.dumpRecorded) { slot.dumpRecorded = false; WriteFrameDump(slot); @@ -806,7 +825,7 @@ struct VulkanDevice::State { for (std::uint32_t x = 0; x < width; ++x) std::memcpy(pixels.data() + (static_cast(y) * width + x) * 4, full.data() + (static_cast(y) * scale * slot.dumpWidth + static_cast(x) * scale) * 4, 4); } static const auto start = std::chrono::steady_clock::now(); - std::fprintf(stderr, "[gpu] frame_%03d.bmp: %ux%u display read back on the GPU at %.1f s\n", index, slot.dumpWidth, slot.dumpHeight, std::chrono::duration(std::chrono::steady_clock::now() - start).count()); + AgcDriver::ProfilePrint_nid_no_patch("[gpu] frame_%03d.bmp: %ux%u display read back on the GPU at %.1f s\n", index, slot.dumpWidth, slot.dumpHeight, std::chrono::duration(std::chrono::steady_clock::now() - start).count()); dumpWriter.Enqueue(index, width, height, 1, false, std::move(pixels)); } @@ -902,6 +921,7 @@ struct VulkanDevice::State { detiler.reset(); colorTransfer.reset(); overlay.reset(); + planePass.reset(); presentation.reset(); scaler.reset(); hdrScaler.reset(); @@ -933,6 +953,7 @@ struct VulkanDevice::State { destroyPool(device, pool, nullptr); } if (settlePool != VK_NULL_HANDLE) destroyPool(device, settlePool, nullptr); + Graphics::DestroyDeferredObjects(device); destroyDevice(device, nullptr); } if (instance != VK_NULL_HANDLE) { @@ -1271,10 +1292,12 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver}; state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &driverProperties); driverId = driver.driverID; + state->deferDestruction = driver.driverID == VK_DRIVER_ID_MOLTENVK && std::getenv("APS5_NO_DEFERRED_DESTRUCTION") == nullptr; state->driverInfo = std::string(driver.driverName) + " " + driver.driverInfo; state->driverId = driver.driverID; state->individuallyFreeDescriptorSets = driver.driverID == VK_DRIVER_ID_MESA_LLVMPIPE; state->fusedExactArithmetic = driver.driverID == VK_DRIVER_ID_MOLTENVK; + state->tessellationPipelineCache = TessellationPipelineCacheUsable(driver.driverID); state->subgroupControlBarriers = SubgroupControlBarrierUsable(driver.driverID); state->workgroupScopeFences = WorkgroupScopeFenceUsable(driver.driverID); } @@ -1682,7 +1705,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : queueInfo.queueFamilyIndex = family; queueInfo.queueCount = 1; queueInfo.pQueuePriorities = &priority; - const float priorities[2]{1.0f, 1.0f}; + const float priorities[3]{1.0f, 1.0f, 1.0f}; #ifdef _WIN32 const bool settles = true; #else @@ -1729,13 +1752,13 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : // Guest shaders routinely read past descriptor ranges; robust access turns that into zeros // instead of a GPU fault that loses the device. enabled.robustBufferAccess = available.robustBufferAccess; - const auto plan = PlanDeviceQueues(queues, family, settles, available.sparseBinding == VK_TRUE); + const auto plan = PlanDeviceQueues(queues, family, settles, available.sparseBinding == VK_TRUE, window != nullptr); const bool settleQueue = plan.settle; const auto settleFamily = plan.settleFamily; const auto settleIndex = plan.settleIndex; VkDeviceQueueCreateInfo queueInfos[3]{queueInfo, {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}, {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}}; queueInfos[0].queueCount = plan.mainCount; - queueInfos[0].pQueuePriorities = plan.mainCount == 2 ? priorities : &priority; + queueInfos[0].pQueuePriorities = plan.mainCount > 1 ? priorities : &priority; deviceInfo.queueCreateInfoCount = 1; const auto addQueue = [&](std::uint32_t queueFamily, std::uint32_t index) { for (std::uint32_t info = 1; info < deviceInfo.queueCreateInfoCount; ++info) { @@ -2082,6 +2105,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : check(state->InstanceFunction("vkCreateDevice")(selected, &deviceInfo, nullptr, &state->device), "vkCreateDevice"); state->shaderProfile = std::move(shaderProfile); if (compileOnly) return; + if (state->deferDestruction) state->contextDeviceProc = Graphics::DeferDestructionToIdle(state->device, state->deviceProc); if (calibratedTimestamps != nullptr) { const bool khr = std::strcmp(calibratedTimestamps, VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME) == 0; const auto domains = reinterpret_cast(state->instanceProc(state->instance, khr ? "vkGetPhysicalDeviceCalibrateableTimeDomainsKHR" : "vkGetPhysicalDeviceCalibrateableTimeDomainsEXT")); @@ -2103,6 +2127,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : } state->DeviceFunction("vkGetDeviceQueue")(state->device, family, 0, &state->queue); state->queueFamily = family; + if (plan.present) { + state->DeviceFunction("vkGetDeviceQueue")(state->device, family, plan.presentIndex, &state->presentQueue); + APS5_LOG_OUT("Presentation queue family=%u index=%u", family, plan.presentIndex); + } if (settleQueue) { state->settleFamily = settleFamily; state->DeviceFunction("vkGetDeviceQueue")(state->device, settleFamily, settleIndex, &state->settleQueue); @@ -2193,7 +2221,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : require((surface.supportedUsageFlags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) != 0, "surface does not support color attachment images"); VkSwapchainCreateInfoKHR swapchain{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR}; swapchain.surface = state->surface; - swapchain.minImageCount = surface.minImageCount; + swapchain.minImageCount = AgcDriver::SwapchainImageCount(surface); swapchain.imageFormat = state->surfaceFormat; swapchain.imageColorSpace = state->surfaceColorSpace; swapchain.imageExtent = state->extent; @@ -2297,6 +2325,26 @@ bool VulkanDevice::ReplayComputePipeline(const ShaderRecompiler::RecompileResult return state->computePipelines.try_emplace(key, std::move(objects)).second; } +bool VulkanDevice::HasComputePipeline(const ShaderRecompiler::RecompileResult& shader) const { + const std::array shaders{{{ShaderRecompiler::ShaderStage::Compute, &shader, 0}}}; + const auto key = ComputePipelineKey::Of(shader, Graphics::PushConstantStages(shaders) != 0); + std::lock_guard pipelines(state->computePipelinesMutex); + return !key.identity.Empty() && (state->computePipelines.contains(key) || state->computePipelinesBuilding.contains(key)); +} + +void VulkanDevice::PrebuildComputePipeline(const ShaderRecompiler::RecompileResult& shader, std::uint64_t programAddress) { + if (shader.spirv.size() < 5 || shader.spirv[0] != 0x07230203u) return; + ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); + const auto context = graphicsContext(); + if (context.descriptorCache == nullptr) return; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Compute, &shader, 0}}}; + const auto pushStages = Graphics::PushConstantStages(shaders); + if (pushStages != 0 && state->properties.limits.maxPushConstantsSize < Graphics::ComputePushConstantBytes) return; + if (HasComputePipeline(shader)) return; + const auto layouts = Graphics::DescribeSetLayouts(context, shaders); + static_cast(computePipeline(context, shader, layouts.layouts.main, layouts.mainKey, pushStages, programAddress)); +} + bool VulkanDevice::ImportGuestMemory(const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice) { if (adoptDevice) GuestMemory::AssertGpuLockHeld("VulkanDevice::ImportGuestMemory"); return Graphics::ImportMappedRanges(graphicsContext(), ranges, generation, adoptDevice); @@ -2325,6 +2373,7 @@ void VulkanDevice::PrepareForReplacement() { void VulkanDevice::WaitIdle() { APS5_LOG_CHARS_OUT_DEBUG("VulkanDevice::WaitIdle begin"); + static_cast(Graphics::StorageTexture::StoreDrawnLinear("wait idle")); RetirePresents(0); if (state->recorder) { // Announced with this call's return address: the [recorder] site table then names the @@ -2332,6 +2381,8 @@ void VulkanDevice::WaitIdle() { if (!state->recorder->Idle()) Graphics::Recorder::CountSync(0, __builtin_return_address(0)); state->recorder->Sync(); } + std::unique_lock presentQueueLock(state->presentQueueMutex, std::defer_lock); + if (state->presentQueue != VK_NULL_HANDLE) presentQueueLock.lock(); check(state->DeviceFunction("vkDeviceWaitIdle")(state->device), "vkDeviceWaitIdle"); Graphics::CheckPixelFunctionLds(graphicsContext()); APS5_LOG_CHARS_OUT_DEBUG("VulkanDevice::WaitIdle complete"); @@ -2429,6 +2480,7 @@ bool VulkanDevice::StoresPendingOver(std::uint64_t address, std::size_t bytes) c int VulkanDevice::WriteLabelOnGpu(std::uint64_t address, std::span bytes, std::uint64_t stamp, std::uint32_t queue, bool reapFirst) { static const bool storeAtLabel = Graphics::StorageTexture::StoreAtLabelRequested(std::getenv("APS5_STORE_AT_LABEL")); if (storeAtLabel) Graphics::StorageTexture::FlushAllPending("label"); + if (queue == 0) static_cast(Graphics::StorageTexture::StoreDrawnLinear("label")); Graphics::StorageTexture::StoreReadbackBeforeLabel("readback before label"); if (!state->recorder || bytes.empty() || bytes.size() > 65536 || address % 4 != 0 || bytes.size() % 4 != 0) return 4; auto& recorder = *state->recorder; @@ -3044,7 +3096,7 @@ void VulkanDevice::recreateSwapchain(std::uint32_t width, std::uint32_t height, require((surface.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0 && (surface.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR) != 0 && (surface.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) != 0, "resized surface capabilities are unsupported"); VkSwapchainCreateInfoKHR create{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR}; create.surface = state->surface; - create.minImageCount = surface.minImageCount; + create.minImageCount = AgcDriver::SwapchainImageCount(surface); create.imageFormat = state->surfaceFormat; create.imageColorSpace = state->surfaceColorSpace; create.imageExtent = {width, height}; @@ -3158,9 +3210,9 @@ bool VulkanDevice::Presentable() const { return state->extent.width != 0 && state->extent.height != 0; } -bool VulkanDevice::PresentClear(std::uint32_t width, std::uint32_t height, bool opaque) { +bool VulkanDevice::PresentClear(std::uint32_t width, std::uint32_t height, bool opaque, const DisplayBuffer* plane) { APS5_LOG_OUT_DEBUG("PresentClear width=%u height=%u opaque=%u", width, height, static_cast(opaque)); - return present(width, height, opaque, {}); + return present(width, height, opaque, {}, nullptr, nullptr, VK_FILTER_LINEAR, false, false, nullptr, 0, false, plane); } void VulkanDevice::PresentPixels(std::uint32_t width, std::uint32_t height, std::span pixels) { @@ -3291,6 +3343,56 @@ bool NoResidentPresent() { return no; } +// 64 KiB blocks the CPU wrote since the image last matched guest memory would be shown stale (the +// write-back keeps the CPU's bytes for them): Refresh merges them into the image first, as a sampled +// texture of the image would (see cachedTexture). The uncached walk: the collect memo is per worker +// packet, so on this thread a memoized answer could miss a CPU write that landed after a worker's +// walk of the same range. +bool RefreshPresented(Graphics::StorageTexture& resident, const DisplayBuffer& buffer, std::uint32_t layer) { + static const bool noAdopt = std::getenv("APS5_NO_PRESENT_EPOCH") != nullptr; + const auto bytes = DisplayBufferSize(buffer); + if (!noAdopt) GuestMemory::AdoptLatestCollectEpoch(); + GuestMemory::CollectWritesUncached(buffer.address, bytes); + if (GuestMemory::UnchangedSince(buffer.address, bytes, resident.GenerationFor(buffer.address, bytes)) && (buffer.dccAddress == 0 || !ResidentKeysMoved(resident))) return false; + if (resident.ImageLayers() > 1) resident.RefreshLayers(layer, 1); + else resident.Refresh(); + return true; +} + +struct PlaneSource { + std::shared_ptr resident; + std::uint32_t layer = 0; + std::vector texels; +}; + +PlaneSource OverlayPlaneSource(const Graphics::Context& context, const DisplayBuffer& buffer) { + require(DisplayTexelBytes(buffer.pixelFormat) == 4u && !DisplayTenBit(buffer.pixelFormat) && !DisplayPq(buffer.pixelFormat), "VideoOut: only an 8-bit SDR overlay plane is composited over the main output"); + PlaneSource source; + if (!NoResidentPresent()) { + VkFilter filter = VK_FILTER_LINEAR; + bool pending = false; + bool convert = false; + source.resident = PresentableResident(context, buffer, filter, pending, convert, source.layer); + if (source.resident != nullptr && buffer.dccAddress != 0 && !ResidentServesDisplay(*source.resident, buffer)) source.resident.reset(); + if (source.resident != nullptr) { + static_cast(RefreshPresented(*source.resident, buffer, source.layer)); + return source; + } + } + const auto bytes = DisplayBufferSize(buffer); + GuestMemory::FlushGpuWrites(buffer.address, bytes); + const auto keys = buffer.dccAddress != 0 ? DisplayBufferKeys(buffer) : Graphics::DccKeys::Uncompressed; + if (keys == Graphics::DccKeys::Uncompressed) { + GuestMemory::CheckRange(reinterpret_cast(buffer.address), bytes, buffer.alignment); + source.texels = DisplayBufferTexels(buffer, {reinterpret_cast(buffer.address), bytes}); + return source; + } + const auto texel = DisplayBufferClearTexel(buffer, keys); + source.texels.resize(static_cast(buffer.width) * buffer.height * texel.size()); + for (std::size_t offset = 0; offset < source.texels.size(); offset += texel.size()) std::memcpy(source.texels.data() + offset, texel.data(), texel.size()); + return source; +} + // Debug aid: APS5_DUMP_FRAMES= saves the first n presented display buffers as frame_.bmp, // read back by the blit's own submission and written after its fence (RetireSlot), or with // APS5_NO_GPU_DUMP=1 decoded from the tiled guest buffer on the CPU, as before. @@ -3315,7 +3417,16 @@ FrameDumps& Dumps() { } -bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { +bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer, const DisplayBuffer* plane) { + if (buffer.dccAddress != 0 && buffer.tilingMode == 0) { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + const auto before = profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0; + state->recorder->SyncThrough(buffer.dccAddress, DisplayBufferKeyBytes(buffer), true); + if (const auto waited = profile ? Graphics::Recorder::ThreadWaitedMs() - before : 0.0; waited > 0.0) { + ++presentCounters.keyWriterWaits; + presentCounters.keyWriterWaitMs += waited; + } + } static const bool storeAtFlip = Graphics::StorageTexture::StoreAtFlipRequested(std::getenv("APS5_STORE_AT_FLIP")); if (storeAtFlip) Graphics::StorageTexture::FlushAllPending("flip"); else Graphics::StorageTexture::FlushDumpSelected("flip"); @@ -3338,7 +3449,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { } const bool screenshot = takeScreenshot(dumpFrame); auto shot = screenshot && passthrough ? ReadDisplayBuffer(buffer) : std::vector{}; - if (!present(buffer.width, buffer.height, true, pixels, nullptr, nullptr, VK_FILTER_LINEAR, dumpFrame || (screenshot && !passthrough), false, nullptr, 0, passthrough)) { + if (!present(buffer.width, buffer.height, true, pixels, nullptr, nullptr, VK_FILTER_LINEAR, dumpFrame || (screenshot && !passthrough), false, nullptr, 0, passthrough, plane)) { if (dumpFrame) --dumps.dumped; if (screenshot) Screenshot::Request(); return false; @@ -3370,19 +3481,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { } else if (resident == nullptr) { ++unsuitable; } else { - // 64 KiB blocks the CPU wrote since the image last matched guest memory would be shown - // stale (the write-back keeps the CPU's bytes for them): Refresh merges them into the - // image first, as a sampled texture of the image would (see cachedTexture). The uncached - // walk: the collect memo is per worker packet, so on this thread a memoized answer could - // miss a CPU write that landed after a worker's walk of the same range. - static const bool noAdopt = std::getenv("APS5_NO_PRESENT_EPOCH") != nullptr; - if (!noAdopt) GuestMemory::AdoptLatestCollectEpoch(); - GuestMemory::CollectWritesUncached(buffer.address, bytes); - if (!GuestMemory::UnchangedSince(buffer.address, bytes, resident->GenerationFor(buffer.address, bytes)) || (buffer.dccAddress != 0 && ResidentKeysMoved(*resident))) { - if (resident->ImageLayers() > 1) resident->RefreshLayers(layer, 1); - else resident->Refresh(); - ++refreshedPresents; - } + if (RefreshPresented(*resident, buffer, layer)) ++refreshedPresents; ++residentPresents; } } @@ -3413,7 +3512,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { const auto channel = [&](std::size_t index) { return static_cast(std::to_integer((*cleared)[index])) / 255.0f; }; uniform = {{channel(2), channel(1), channel(0), channel(3)}}; } - if (!present(buffer.width, buffer.height, true, {}, cleared ? nullptr : &buffer, resident, filter, dumpFrame || (screenshot && !cpuScreenshot), convert, cleared ? &uniform : nullptr, layer, passthrough)) { + if (!present(buffer.width, buffer.height, true, {}, cleared ? nullptr : &buffer, resident, filter, dumpFrame || (screenshot && !cpuScreenshot), convert, cleared ? &uniform : nullptr, layer, passthrough, plane)) { // A dropped frame (swapchain out of date) keeps the dump numbering contiguous. if (dumpFrame) --dumps.dumped; if (screenshot) Screenshot::Request(); @@ -3469,7 +3568,7 @@ bool VulkanDevice::AcquireImage() { return true; } -bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display, const std::shared_ptr& resident, VkFilter residentFilter, bool dumpFrame, bool residentConvert, const VkClearColorValue* uniform, std::uint32_t residentLayer, bool passthrough) { +bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display, const std::shared_ptr& resident, VkFilter residentFilter, bool dumpFrame, bool residentConvert, const VkClearColorValue* uniform, std::uint32_t residentLayer, bool passthrough, const DisplayBuffer* plane) { PerformanceTimer timing("Vulkan.Present"); APS5_LOG_OUT_DEBUG("present begin width=%u height=%u opaque=%u pixels=%zu", width, height, static_cast(opaque), pixels.size()); require(state->swapchain != VK_NULL_HANDLE, "device has no swapchain"); @@ -3503,6 +3602,12 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu auto& slot = state->presentSlots[state->presentCursor]; require(!slot.inFlight, "presentation slot is still in flight"); const bool overlay = PerformanceOverlay::Visible(); + if (plane != nullptr && state->surfaceColorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT) { + static std::once_flag reported; + std::call_once(reported, [] { std::fprintf(stderr, "[present] the overlay plane is not composited over an HDR10 presentation\n"); }); + plane = nullptr; + } + const auto planeSource = plane != nullptr ? OverlayPlaneSource(graphicsContext(), *plane) : PlaneSource{}; if (display != nullptr && resident == nullptr) { static_cast(DisplayBufferSize(*display)); state->colorTransfer->Upload(display->address, width, height, Graphics::ColorTileMode::RenderTarget, DisplayTexelBytes(display->pixelFormat)); @@ -3547,6 +3652,34 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu barrier.image = state->images[index]; barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; auto pipelineBarrier = state->DeviceFunction("vkCmdPipelineBarrier"); + const auto planeSlot = static_cast(&slot - state->presentSlots.data()); + VkImageMemoryBarrier planeBarrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + bool planeBound = false; + const auto bindPlane = [&] { + if (planeBound) return; + planeBound = true; + if (!state->planePass) state->planePass = std::make_unique(graphicsContext(), state->presentSlots.size()); + if (planeSource.resident == nullptr) { + state->planePass->BindTexels(planeSlot, commands, planeSource.texels, plane->width, plane->height, plane->pixelFormat, whiteNits); + return; + } + planeBarrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; + planeBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + planeBarrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL; + planeBarrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; + planeBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + planeBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + planeBarrier.image = planeSource.resident->Image(); + planeBarrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, VK_REMAINING_MIP_LEVELS, 0, VK_REMAINING_ARRAY_LAYERS}; + pipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &planeBarrier); + Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); + state->planePass->Bind(planeSlot, planeSource.resident->Image(), plane->width, plane->height, plane->pixelFormat, whiteNits, planeSource.layer); + }; + const auto blendPlaneInto = [&](VkImage image, VkFormat format, VkExtent2D extent) { + if (plane == nullptr) return; + bindPlane(); + state->planePass->RecordOverlay(commands, planeSlot, image, format, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + }; if (!drawn) { pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); @@ -3594,7 +3727,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu } if (drawn) { state->presentation->Bind(slotIndex, resident->Image(), width, height, display->pixelFormat, whiteNits, residentLayer); - state->presentation->RecordPresent(commands, slotIndex, barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, overlay ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + state->presentation->RecordPresent(commands, slotIndex, barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, overlay || plane != nullptr ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); } else { scaler->RecordLetterbox(commands, width, height, barrier.image, state->extent.width, state->extent.height); if (direct) PresentationScaler::RecordBlitFrom(graphicsContext(), commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, width, height, residentFilter, barrier.image, state->extent.width, state->extent.height, residentLayer); @@ -3615,13 +3748,17 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu const auto dumpBytes = static_cast(slot.dumpWidth) * slot.dumpHeight * 4; if (!slot.dumpBuffer || slot.dumpBuffer->Bytes().size() != dumpBytes) slot.dumpBuffer = std::make_unique(graphicsContext(), dumpBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); if (drawn) { - state->presentation->RecordDump(commands, slotIndex, scale, slot.dumpBuffer->Handle()); + state->presentation->RecordDumpImage(commands, slotIndex, scale); + blendPlaneInto(state->presentation->DumpImage(slotIndex), VK_FORMAT_B8G8R8A8_UNORM, state->presentation->DumpImageExtent(slotIndex)); + state->presentation->RecordDumpReadback(commands, slotIndex, slot.dumpBuffer->Handle()); } else if (direct) { if (!slot.dumpScaler) slot.dumpScaler = std::make_unique(graphicsContext(), VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM); slot.dumpScaler->EnsureSourceImage(slot.dumpWidth, slot.dumpHeight); slot.dumpScaler->RecordBlitInto(commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, VK_FILTER_NEAREST, width, height, residentLayer); + blendPlaneInto(slot.dumpScaler->SourceImage(), VK_FORMAT_B8G8R8A8_UNORM, {slot.dumpWidth, slot.dumpHeight}); slot.dumpScaler->RecordReadback(commands, slot.dumpBuffer->Handle()); } else { + blendPlaneInto(scaler->SourceImage(), scaler->SourceFormat(), {scaler->SourceWidth(), scaler->SourceHeight()}); scaler->RecordReadback(commands, slot.dumpBuffer->Handle()); } } @@ -3632,11 +3769,21 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); } } + if (plane != nullptr) { + bindPlane(); + state->planePass->RecordOverlay(commands, planeSlot, barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, drawn ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, overlay ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + if (planeSource.resident != nullptr) { + planeBarrier.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + planeBarrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; + pipelineBarrier(commands, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &planeBarrier); + Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); + } + } if (overlay) { if (!state->overlay) state->overlay = std::make_unique(graphicsContext(), state->presentSlots.size()); state->overlay->NotePresent(std::chrono::steady_clock::now(), clearOnly ? VkExtent2D{} : VkExtent2D{width, height}); - state->overlay->Record(commands, static_cast(&slot - state->presentSlots.data()), barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, drawn ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); - } else if (!drawn) { + state->overlay->Record(commands, static_cast(&slot - state->presentSlots.data()), barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, drawn || plane != nullptr ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + } else if (!drawn && plane == nullptr) { barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; barrier.dstAccessMask = 0; barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; @@ -3664,6 +3811,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu // The image stays alive until the slot is retired (storage-cache eviction only drops the // cache's reference). slot.kept = resident; + slot.keptPlane = planeSource.resident; slot.imageIndex = index; slot.inFlight = true; slot.shared = shared; @@ -3679,6 +3827,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu state->queuePending = true; state->queueIndex = index; ++presentCounters.presents; + if (plane != nullptr) ++presentCounters.planes; if (auto* frame = PerformanceContext::Current()) { frame->NoteBlit(batchesAtBlit, lastSubmittedAt, submittedAt); // The serials are cumulative: only their difference (the batches other queues put @@ -3780,13 +3929,19 @@ void VulkanDevice::QueuePresent() { present.swapchainCount = 1; present.pSwapchains = &state->swapchain; present.pImageIndices = &index; - const auto presented = state->DeviceFunction("vkQueuePresentKHR")(state->queue, &present); + std::unique_lock presentQueueLock(state->presentQueueMutex, std::defer_lock); + if (state->presentQueue != VK_NULL_HANDLE) presentQueueLock.lock(); + const auto presented = state->DeviceFunction("vkQueuePresentKHR")(state->presentQueue != VK_NULL_HANDLE ? state->presentQueue : state->queue, &present); if (presented == VK_ERROR_OUT_OF_DATE_KHR || presented == VK_SUBOPTIMAL_KHR) state->extent = {0, 0}; else check(presented, "vkQueuePresentKHR"); timing.Mark("queue_present"); APS5_LOG_OUT_DEBUG("vkQueuePresentKHR queued imageIndex=%u", index); } +bool VulkanDevice::PresentsOnOwnQueue() const { + return state->presentQueue != VK_NULL_HANDLE; +} + ShaderRecompiler::SpirvTarget VulkanDevice::ComputeTarget(std::uint32_t waveSize) const { auto target = Target(); if (waveSize == 32u && state->computeWave32) target.subgroupSize = 32u; @@ -3930,7 +4085,7 @@ Graphics::Context VulkanDevice::buildContext() const { state->physical, state->queue, state->pool, - state->deviceProc, + state->contextDeviceProc != nullptr ? state->contextDeviceProc : state->deviceProc, state->InstanceFunction("vkGetPhysicalDeviceFormatProperties"), state->InstanceFunction("vkGetPhysicalDeviceImageFormatProperties"), state->memoryProperties, @@ -3955,6 +4110,7 @@ Graphics::Context VulkanDevice::buildContext() const { }; context.pipelineCacheSynchronization = state->pipelineCache ? &state->pipelineCache->Synchronization() : nullptr; context.individuallyFreeDescriptorSets = state->individuallyFreeDescriptorSets; + context.tessellationPipelineCache = state->tessellationPipelineCache; context.dmaBufImport = state->dmaBufImport; context.sparseBinding = state->sparseBinding; context.sparseResidencyBuffer = state->sparseResidencyBuffer; @@ -4332,7 +4488,7 @@ struct DispatchCounters { std::array phaseTotals{}; std::uint64_t profiledDispatches = 0; IndirectHold indirectHold; - std::uint64_t cacheHits = 0, cacheMisses = 0, cacheInvalidated = 0, templateRefreshed = 0, templateSameWords = 0; + std::uint64_t cacheHits = 0, cacheMisses = 0, cacheInvalidated = 0, templateRefreshed = 0, templateSameWords = 0, templateByNumber = 0; double templateRevalidateMs = 0; std::uint64_t procLookups = 0, preBarriersRecorded = 0, preBarriersSkipped = 0; std::chrono::steady_clock::time_point cacheReport = std::chrono::steady_clock::now(); @@ -4975,6 +5131,7 @@ 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())); VkAccessFlags covered = 0; const auto commands = recorder.Commands(&covered); recordStep(PhaseRecordCommands); @@ -4993,6 +5150,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { // after an earlier dispatch's reads of the buffers by that dispatch's trailing barrier. A // recipe compares the two 64-bit hashes first: equal hashes mean the buffers hold the words. const bool differs = record.dataRefresh == RecordedDispatch::DataRefresh::Words || resources.DataWordsHash() != record.dataWordsHash; + if (timer.profile && record.shader->program != nullptr && record.shader->program->bindings.size() != resources.BindingCount()) ++d.templateByNumber; if (differs && (record.resourceKeyMatched ? resources.refreshData(commands, *record.shader, &recorder) : resources.RefreshData(commands, *record.shader, &recorder))) { covered = 0; ++d.templateRefreshed; @@ -5506,7 +5664,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec } if (profile && std::chrono::steady_clock::now() - d.cacheReport > std::chrono::seconds(10)) { d.cacheReport = std::chrono::steady_clock::now(); - AgcDriver::ProfilePrint_nid_no_patch("[rescache] (cumulative): %llu hits, %llu misses, %llu invalidated, %zu entries (dispatch + draw); template hits: %llu refreshed the data buffers (revalidate %.1f ms), %llu had the same words; Find calls %llu, Touch calls %llu\n", static_cast(d.cacheHits), static_cast(d.cacheMisses), static_cast(d.cacheInvalidated), state->resourceCache.Size(), static_cast(d.templateRefreshed), d.templateRevalidateMs, static_cast(d.templateSameWords), static_cast(ResourceCache::Finds()), static_cast(ResourceCache::Touches())); + AgcDriver::ProfilePrint_nid_no_patch("[rescache] (cumulative): %llu hits, %llu misses, %llu invalidated, %zu entries (dispatch + draw); template hits: %llu refreshed the data buffers (revalidate %.1f ms), %llu had the same words, %llu paired their data bindings by number (no image table binding of their own); Find calls %llu, Touch calls %llu\n", static_cast(d.cacheHits), static_cast(d.cacheMisses), static_cast(d.cacheInvalidated), state->resourceCache.Size(), static_cast(d.templateRefreshed), d.templateRevalidateMs, static_cast(d.templateSameWords), static_cast(d.templateByNumber), static_cast(ResourceCache::Finds()), static_cast(ResourceCache::Touches())); AgcDriver::ProfilePrint_nid_no_patch("[vk] deviceProc lookups inside the device call (cumulative): %llu (%.2f per dispatch); pre-dispatch barriers recorded %llu, skipped %llu; compute pipelines created %llu (%llu under the GPU mutex)\n", static_cast(d.procLookups), d.profiledDispatches != 0 ? static_cast(d.procLookups) / static_cast(d.profiledDispatches) : 0.0, static_cast(d.preBarriersRecorded), static_cast(d.preBarriersSkipped), static_cast(computePipelinesCreated.load(std::memory_order_relaxed)), static_cast(computePipelinesCreatedLocked.load(std::memory_order_relaxed))); reportRecipes(); reportDispatchMove(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp index f95ac3a62f..3bdac581be 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp @@ -163,18 +163,29 @@ struct Sampler { return frame; } + static void writeModules(std::FILE* file, const std::map& loaded) { + for (const auto& [base, name] : loaded) { + const auto* dos = reinterpret_cast(base); + const auto* nt = reinterpret_cast(base + dos->e_lfanew); + std::fprintf(file, "# module %s 0x%lx 0x%lx\n", name.c_str(), static_cast(nt->OptionalHeader.SizeOfImage), static_cast(nt->FileHeader.TimeDateStamp)); + } + } + void write() { std::lock_guard lock(mutex); std::FILE* file = std::fopen(path.c_str(), "w"); if (file == nullptr) return; std::fprintf(file, "# %llu samples\n", static_cast(samples)); + std::map loaded; for (const auto& [address, count] : counts) { HMODULE module = nullptr; char name[MAX_PATH] = "?"; if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(address), &module)) GetModuleFileNameA(module, name, sizeof(name)); const char* base = std::strrchr(name, '\\'); + if (module != nullptr) loaded.emplace(reinterpret_cast(module), base ? base + 1 : name); std::fprintf(file, "%s 0x%llx %llu %llu\n", base ? base + 1 : name, static_cast(address - reinterpret_cast(module)), static_cast(count.first), static_cast(count.second)); } + writeModules(file, loaded); std::fclose(file); if (keepStacks) writeStacks(); } @@ -195,11 +206,7 @@ struct Sampler { const double tickUs = 1e6 / static_cast(frequency.QuadPart); std::fprintf(file, "# samples=%llu depth=%d truncated=%llu suspended_us_mean=%.1f suspended_us_max=%.1f\n", static_cast(samples), depth, static_cast(stacks.Truncated()), samples != 0 ? static_cast(suspendedTicks) * tickUs / static_cast(samples) : 0.0, static_cast(longestSuspendedTicks) * tickUs); - for (const auto& [base, name] : loaded) { - const auto* dos = reinterpret_cast(base); - const auto* nt = reinterpret_cast(base + dos->e_lfanew); - std::fprintf(file, "# module %s 0x%lx 0x%lx\n", name.c_str(), static_cast(nt->OptionalHeader.SizeOfImage), static_cast(nt->FileHeader.TimeDateStamp)); - } + writeModules(file, loaded); const auto text = stacks.Format([&](std::uint64_t address) { return names.at(address); }); const bool written = std::fwrite(text.data(), 1, text.size(), file) == text.size(); if (std::fclose(file) != 0 || !written) return; @@ -246,6 +253,7 @@ struct ProcessSampler { std::FILE* file = std::fopen(path.c_str(), "w"); if (file == nullptr) return; std::fprintf(file, "# %llu rounds\n", static_cast(rounds)); + std::map loaded; for (auto& [id, thread] : threads) { std::lock_guard lock(thread.mutex); std::vector>> hot(thread.counts.begin(), thread.counts.end()); @@ -260,9 +268,11 @@ struct ProcessSampler { char name[MAX_PATH] = "?"; if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(address), &module)) GetModuleFileNameA(module, name, sizeof(name)); const char* base = std::strrchr(name, '\\'); + if (module != nullptr) loaded.emplace(reinterpret_cast(module), base ? base + 1 : name); std::fprintf(file, " %s 0x%llx %llu %llu\n", base ? base + 1 : name, static_cast(address - reinterpret_cast(module)), static_cast(hot[i].second.first), static_cast(hot[i].second.second)); } } + Sampler::writeModules(file, loaded); std::fclose(file); } }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 7e0e07d07b..dc2e00475a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -240,6 +240,7 @@ struct Context { std::uint32_t imageTableCapacity = 0; std::uint32_t samplerTableCapacity = 0; bool individuallyFreeDescriptorSets = false; + bool tessellationPipelineCache = true; bool imageTableUpdateAfterBind = false; bool samplerTableUpdateAfterBind = false; bool geometryShader = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DeferredDestruction.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DeferredDestruction.hpp new file mode 100644 index 0000000000..70fb764122 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DeferredDestruction.hpp @@ -0,0 +1,17 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_DEFERREDDESTRUCTION_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_DEFERREDDESTRUCTION_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include +#include + +namespace AgcDriver::Graphics { + +PFN_vkGetDeviceProcAddr DeferDestructionToIdle(VkDevice device, PFN_vkGetDeviceProcAddr resolver); +void DestroyDeferredAtIdle(const Context& context, std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now()); +void DestroyDeferredObjects(VkDevice device) noexcept; +std::size_t DeferredObjects(VkDevice device); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 9fe4caae06..6cc9174ea6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -38,6 +38,7 @@ struct HostImport { // Identity for the life of this import (see HostImportSerial); 0 until first asked for. std::uint64_t serial = 0; bool unwatched = false; + bool settleOnDemand = false; bool dmaBuf = false; bool joined = false; std::shared_ptr chunk; @@ -102,6 +103,7 @@ void ReportHostImports(); // address)` in the import's buffer. const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes, ImportRequester requester = ImportRequester::Access); const HostImport* ExistingHostImportFor(const Context& context, std::uint64_t address, std::size_t bytes); +bool WatchedAfterSettle(const Context& context, std::uint64_t address, std::size_t bytes); void SetHostImportWindow(std::uint64_t bytes); // Tests: the window of one large registered range (64 MiB) and the import budget (0: the configured one). void SetHostImportRangeWindow(std::uint64_t bytes); @@ -133,6 +135,7 @@ bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& // made outside the device lock (a sampled texture's path, a dispatch's pre-sync); the path taken // re-checks with HostImportFor when it binds the import. bool HostImportCovers(const Context& context, std::uint64_t address, std::size_t bytes); +bool ComparedHostImportCovers(const Context& context, std::uint64_t address, std::size_t bytes); struct RegisteredPiece { std::uint64_t address; std::uint64_t bytes; @@ -264,6 +267,8 @@ struct AddressCopy { std::uint64_t end; std::uint64_t committed; const char* reason; + bool heap = false; + bool overBudget = false; }; std::string AddressCopyOverflow(std::vector copies, std::uint64_t limit); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index abfb3226a6..b9cea62862 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -109,8 +109,10 @@ private: // stages and every pipeline state field. A configuration whose stage has no module gets a private // pipeline. A miss builds the pipeline outside the store's mutex (a concurrent build of the same // key keeps the first inserted) and records its recipe in the pipeline journal. Entries are -// LRU-bounded (APS5_PIPELINE_STORE_ENTRIES, 1024) and only evicted once no recorded draw holds -// them. Debug aid: APS5_NO_PIPELINE_CACHE=1 builds one per draw as before. +// LRU-bounded and only evicted once no recorded draw holds them. The bound starts at 1024 and +// doubles up to 4096 for a journal replay that does not fit and while pipelines it evicted keep +// coming back; it never shrinks during a run. APS5_PIPELINE_STORE_ENTRIES fixes it. Debug aid: +// APS5_NO_PIPELINE_CACHE=1 builds one per draw as before. std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); // `created` reports whether this call built the pipeline (false: served from the store or private). std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, bool* created = nullptr); @@ -124,6 +126,8 @@ struct PipelineStoreCounters { std::uint64_t bound = 0; }; PipelineStoreCounters PipelineStoreTotals(); +// Whether the store may still raise its bound (not fixed by APS5_PIPELINE_STORE_ENTRIES, below 4096). +bool PipelineStoreCanGrow(); // The pipeline store's key of a configuration (empty when the configuration gets a private pipeline). void BuildPipelineStoreKey(std::vector& key, const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout); struct DescribedSetLayouts { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 8f1c131eba..f76e061983 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -183,6 +183,7 @@ public: // Whether the OPEN (unsubmitted) batch writes the range: a wait on such a range must submit it. bool OpenWriteOverlaps(std::uint64_t address, std::size_t bytes) const; std::uint64_t PendingWriterSerial(std::uint64_t address, std::size_t bytes) const; + std::uint64_t PendingWriterSerial(std::uint64_t address, std::size_t bytes, std::span exceptNotes) const; // What SyncThrough(address, bytes) would wait for, for the [hooksync] attribution: the newest // batch whose noted range overlaps the access (the open batch counts first, with the serial it // will get), whether that batch's fence has already signaled (its GPU work is done, so the diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index b3d8776df4..2438745430 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -32,6 +32,7 @@ void FlushCachedTextures(VkDevice device); void ClearCachedTextures(VkDevice device); std::uint64_t ReleaseTextureCaches(std::uint64_t bytes, bool newImage = false); std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory); +std::uint64_t StorageTextureCeiling(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported); std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t sampledBytes, std::uint64_t storageBytes); std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes, std::uint64_t storageBytes = 0); bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport); @@ -315,6 +316,7 @@ public: } // Whether RefreshData would record anything for `shader` (the per-word compare; verification). bool DataWordsDiffer(const CompiledShader& shader) const; + std::size_t BindingCount() const { return bindings.size(); } // Why the fast proof of a Revalidate left the object to the full walk (the [rescache] // revalidate line's reasons); Count: it did not. enum class FastFail : std::size_t { NoRecord, Collect, Pending, Evicted, Changed, Keys, ClearedView, StorageKeys, Depth, Replaced, Audit, Count }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 4e7174f582..13d227b8ea 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -45,6 +45,7 @@ struct ColorTarget { ShaderRecompiler::ColorExportPacking packing = ShaderRecompiler::ColorExportPacking::None; // DCC metadata of a compressed target (CB_COLOR_INFO DCC_ENABLE), or 0 (see DccMetadata.hpp). std::uint64_t dccAddress = 0; + std::uint64_t chainDccAddress = 0; bool dccAlphaOnMsb = false; bool dccPipeAligned = false; std::uint64_t cmaskAddress = 0; @@ -180,6 +181,7 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = void ApplyPointSize(const State& state, ShaderRecompiler::ShaderVertexStageInfo& vertex); State DecodeState(const QueueState& queue, bool vertexWorkgroup = false); void ApplyDepthRemap(const Context& context, State& state); +void ApplyDepthBoundsEmulation(const Context& context, State& state, const ShaderRecompiler::ShaderPixelStageInfo& pixel); void NormalizeUnusedViewportDepth(State& state, const ShaderRecompiler::ShaderPixelStageInfo& pixel); std::array SampleLocations(const State& state); std::array PixelSampleLocations(const State& state, const ShaderRecompiler::ShaderPixelStageInfo& pixel); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 0c6aec2abf..85a45ae60e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -204,6 +205,7 @@ public: bool BlockUnits() const { return blockUnits; } std::uint32_t TrackedUnits() const { return trackedLayers; } void MarkSliceDirty(std::uint32_t slice, std::uint32_t count = 1); + bool MarkDrawn(std::uint32_t slice, std::uint32_t count, VkRect2D scissor); void Flush(); bool Dirty() const; // Stores every pending image overlapping the range, except `except`; returns whether any was. @@ -220,6 +222,8 @@ public: static constexpr std::uint64_t SmallStoreBytes = 64 * 1024; static std::size_t StoreSmallPending(const char* reason); static void StoreReadbackBeforeLabel(const char* reason); + static std::size_t StoreDrawnLinear(const char* reason); + static std::size_t StoreDrawnLinearTouching(const char* reason, const std::function& touches, bool all); static bool AnyPending(); static void FlushDumpSelected(const char* reason); static bool StoreAtFlipRequested(const char* value); @@ -482,6 +486,27 @@ private: // (`shadowed` receives those guest ranges, `imported` the pieces written to the import). std::uint64_t uploadWindows(const HostImport& import, std::span> runs, bool discard = false); std::uint64_t writeBackWindows(std::span segments, std::span> keep, std::uint64_t firstStored, std::uint64_t lastStored, std::vector& shadowed, std::vector>& imported, Buffer* save = nullptr); + struct DrawnRect { + std::uint32_t left = 0; + std::uint32_t top = 0; + std::uint32_t right = 0; + std::uint32_t bottom = 0; + }; + struct DrawnArea { + bool drawn = false; + bool rect = false; + std::uint32_t left = 0; + std::uint32_t top = 0; + std::uint32_t right = 0; + std::uint32_t bottom = 0; + std::vector rects; + }; + DrawnArea drawnArea() const; + std::vector> drawnSpans(const DrawnArea& area, std::span> keep) const; + std::uint64_t storeDrawnRect(const DrawnArea& area, std::span segments, std::span> stores, const std::vector& saveUnits); + void addDrawnRect(DrawnRect rect); + void forgetDrawn(); + void noteStore(std::uint64_t note); // Per-layer validity (thin array surfaces whose layers are 64 KiB-aligned guest slices; every // other surface is one tracked layer): a layer's generation is the write generation its guest // bytes were last known to match the image at, and a pending layer holds results (a clear, a @@ -518,6 +543,7 @@ private: // Per 64 KiB tracker block of the surface (block 0 holds the base address): the generation of // the tracked layer it belongs to, for GuestMemory::ChangedBlocks. void blockGenerations(std::vector& generations) const; + std::uint64_t unitBlockGeneration(std::uint32_t unit, std::uint64_t at) const; // Another image of exactly this surface under another storage format with results pending // (block units, uncompressed, still cached), if any: Refresh takes its pending units from its // image on the device (borrowUnits) instead of having them stored and re-read, and the alias @@ -602,6 +628,7 @@ private: std::uint32_t trackedLayers = 1; std::uint64_t trackedLayerBytes = 0; std::vector layerGeneration; + std::vector completionKept; std::vector layerPending; std::vector layerBehind; bool blockUnits = false; @@ -641,6 +668,12 @@ private: VkFormat storageFormat = VK_FORMAT_UNDEFINED; // Results are on the GPU only (guarded by the pending-write registry lock). bool dirty = false; + bool drawnListed = false; + bool drawnWhole = false; + bool drawingTarget = false; + bool pendingFromOthers = false; + std::vector drawnRects; + std::vector storeNotes; 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/TextureFormat.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp index a2625c2e6b..62218d7c9a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp @@ -13,6 +13,7 @@ namespace AgcDriver::Graphics { struct Context; VkFormat ResolveTextureFormat(std::uint32_t guestFormat); +VkFormat DccClearTextureFormat(std::uint32_t guestFormat); VkComponentSwizzle TextureComponentChannel(std::uint32_t guestFormat, VkComponentSwizzle component); std::uint32_t SrgbDecodeFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical); VkFormat SampledTextureFormat(const Context& context, std::uint32_t guestFormat); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp index 5699bf78e1..dafa52d5f5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp @@ -75,7 +75,7 @@ struct SurfaceGeometry { }; SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor); bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t levels); -GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed); +GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed, bool keysReadAsStored); GuestTextureResource ArrayLayerWindow(const GuestTextureResource& viewed, std::uint32_t maxArrayLayers); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index 963da7f4ef..94751531e6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -521,9 +521,9 @@ DccKeys textureClearKeys(const GuestTextureResource& resource, std::uint64_t gue if (const auto known = WaitForKeyWriters(resource, guestBytes)) keys = *known; else keys = currentDccKeys(resource.dccAddress, DccKeyBytes(guestBytes), memoized); if (keys == DccKeys::Uncompressed || (keys == DccKeys::ClearSingle && SingleKeysReadable(resource))) return keys; - if (IsConvertedTextureFormat(resource.format) && (keys == DccKeys::Clear0001 || keys == DccKeys::Clear1110)) throw std::runtime_error(std::string("AGC graphics: DCC clear code ") + DccKeysName(keys) + " of converted texture format " + std::to_string(resource.format) + " is not implemented"); + if (DccClearTextureFormat(resource.format) == ResolveTextureFormat(resource.format) && IsConvertedTextureFormat(resource.format) && (keys == DccKeys::Clear0001 || keys == DccKeys::Clear1110)) throw std::runtime_error(std::string("AGC graphics: DCC clear code ") + DccKeysName(keys) + " of converted texture format " + std::to_string(resource.format) + " is not implemented"); std::byte probe[16]{}; - if (!IsDccClear(keys) || !FillDccClear(ResolveTextureFormat(resource.format), keys, resource.dccAlphaOnMsb, std::span(probe, std::min(sizeof(probe), BytesPerElement(resource.format))))) { + if (!IsDccClear(keys) || !FillDccClear(DccClearTextureFormat(resource.format), keys, resource.dccAlphaOnMsb, std::span(probe, std::min(sizeof(probe), BytesPerElement(resource.format))))) { static AgcDriver::Mutex reportedMutex; static std::set> reported; std::lock_guard lock(reportedMutex); @@ -776,7 +776,7 @@ void ReadTextureSurface(const GuestTextureResource& resource, DccKeys keys, std: // Named for the [hooksync] attribution: the read goes through the flush hook. const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureRead); if (keys == DccKeys::Uncompressed || keys == DccKeys::ClearSingle) GuestMemory::ReadCommitted(resource.baseAddress, bytes); - else FillDccClear(ResolveTextureFormat(resource.format), keys, resource.dccAlphaOnMsb, bytes); + else FillDccClear(DccClearTextureFormat(resource.format), keys, resource.dccAlphaOnMsb, bytes); if (keys == DccKeys::ClearSingle) ExpandSingleBlocks(bytes, BytesPerElement(resource.format)); } @@ -823,7 +823,7 @@ namespace { DccKeys EncodedKeys(const GuestTextureResource& resource, DccKeys keys) { if (keys == DccKeys::ClearSingle && SingleKeysReadable(resource)) return keys; std::byte probe[16]{}; - if (!IsDccClear(keys) || !FillDccClear(ResolveTextureFormat(resource.format), keys, resource.dccAlphaOnMsb, std::span(probe, std::min(sizeof(probe), BytesPerElement(resource.format))))) return DccKeys::Uncompressed; + if (!IsDccClear(keys) || !FillDccClear(DccClearTextureFormat(resource.format), keys, resource.dccAlphaOnMsb, std::span(probe, std::min(sizeof(probe), BytesPerElement(resource.format))))) return DccKeys::Uncompressed; return keys; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DeferredDestruction.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DeferredDestruction.cpp new file mode 100644 index 0000000000..1fb049628a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DeferredDestruction.cpp @@ -0,0 +1,134 @@ +#include "prx/libSceAgcDriver/Graphics/include/DeferredDestruction.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +namespace { + +constexpr std::size_t DrainObjects = 512; +constexpr auto DrainAge = std::chrono::milliseconds(100); + +struct Deferred { + PFN_vkDestroyBuffer destroyBuffer = nullptr; + PFN_vkFreeMemory freeMemory = nullptr; + std::vector buffers; + std::vector memory; + std::chrono::steady_clock::time_point oldest{}; +}; + +struct Registry { + std::mutex mutex; + std::unordered_map devices; + std::atomic resolver{nullptr}; +}; + +Registry& registry() { + static auto& state = *new Registry(); + return state; +} + +void destroyAll(VkDevice device, const Deferred& deferred) noexcept { + for (const auto buffer : deferred.buffers) deferred.destroyBuffer(device, buffer, nullptr); + for (const auto memory : deferred.memory) deferred.freeMemory(device, memory, nullptr); +} + +template +bool defer(VkDevice device, THandle handle, std::vector Deferred::*list) noexcept { + try { + auto& state = registry(); + std::lock_guard lock(state.mutex); + const auto found = state.devices.find(device); + if (found == state.devices.end()) return false; + auto& deferred = found->second; + if (deferred.buffers.empty() && deferred.memory.empty()) deferred.oldest = std::chrono::steady_clock::now(); + (deferred.*list).push_back(handle); + return true; + } catch (...) { + return false; + } +} + +VKAPI_ATTR void VKAPI_CALL deferredDestroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* allocator) { + if (buffer == VK_NULL_HANDLE || defer(device, buffer, &Deferred::buffers)) return; + reinterpret_cast(registry().resolver.load(std::memory_order_acquire)(device, "vkDestroyBuffer"))(device, buffer, allocator); +} + +VKAPI_ATTR void VKAPI_CALL deferredFreeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* allocator) { + if (memory == VK_NULL_HANDLE || defer(device, memory, &Deferred::memory)) return; + reinterpret_cast(registry().resolver.load(std::memory_order_acquire)(device, "vkFreeMemory"))(device, memory, allocator); +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL deferringDeviceProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkDestroyBuffer") == 0) return reinterpret_cast(&deferredDestroyBuffer); + if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast(&deferredFreeMemory); + return registry().resolver.load(std::memory_order_acquire)(device, name); +} + +} + +PFN_vkGetDeviceProcAddr DeferDestructionToIdle(VkDevice device, PFN_vkGetDeviceProcAddr resolver) { + if (device == VK_NULL_HANDLE || resolver == nullptr) throw std::invalid_argument("AGC graphics: deferred destruction needs a device and its resolver"); + Deferred deferred; + deferred.destroyBuffer = reinterpret_cast(resolver(device, "vkDestroyBuffer")); + deferred.freeMemory = reinterpret_cast(resolver(device, "vkFreeMemory")); + if (deferred.destroyBuffer == nullptr || deferred.freeMemory == nullptr) throw std::runtime_error("AGC graphics: missing Vulkan function: vkDestroyBuffer or vkFreeMemory"); + auto& state = registry(); + std::lock_guard lock(state.mutex); + const auto current = state.resolver.load(std::memory_order_acquire); + if (current != nullptr && current != resolver) throw std::runtime_error("AGC graphics: deferred destruction found two different device resolvers"); + if (!state.devices.emplace(device, std::move(deferred)).second) throw std::runtime_error("AGC graphics: deferred destruction was set up twice for one device"); + state.resolver.store(resolver, std::memory_order_release); + return &deferringDeviceProc; +} + +void DestroyDeferredAtIdle(const Context& context, std::chrono::steady_clock::time_point now) { + auto& state = registry(); + { + std::lock_guard lock(state.mutex); + const auto found = state.devices.find(context.device); + if (found == state.devices.end()) return; + const auto objects = found->second.buffers.size() + found->second.memory.size(); + if (objects == 0 || (objects < DrainObjects && now - found->second.oldest < DrainAge)) return; + } + const auto idle = context.Function("vkQueueWaitIdle")(context.queue); + if (idle != VK_SUCCESS && idle != VK_ERROR_DEVICE_LOST) throw std::runtime_error("AGC graphics: vkQueueWaitIdle before destroying deferred objects: Vulkan result " + std::to_string(idle)); + Deferred taken; + { + std::lock_guard lock(state.mutex); + const auto found = state.devices.find(context.device); + if (found == state.devices.end()) return; + taken.destroyBuffer = found->second.destroyBuffer; + taken.freeMemory = found->second.freeMemory; + taken.buffers.swap(found->second.buffers); + taken.memory.swap(found->second.memory); + } + destroyAll(context.device, taken); +} + +void DestroyDeferredObjects(VkDevice device) noexcept { + Deferred taken; + { + auto& state = registry(); + std::lock_guard lock(state.mutex); + const auto found = state.devices.find(device); + if (found == state.devices.end()) return; + taken = std::move(found->second); + state.devices.erase(found); + } + destroyAll(device, taken); +} + +std::size_t DeferredObjects(VkDevice device) { + auto& state = registry(); + std::lock_guard lock(state.mutex); + const auto found = state.devices.find(device); + return found == state.devices.end() ? 0 : found->second.buffers.size() + found->second.memory.size(); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 7273cb82e3..f498c73896 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -719,10 +719,30 @@ public: RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); context.Function("vkCmdCopyImageToBuffer")(commands, storage->Image(), VK_IMAGE_LAYOUT_GENERAL, depthPlane.staging->Handle(), 1, &colorRegion); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); - context.Function("vkCmdCopyBufferToImage")(commands, depthPlane.staging->Handle(), image, VK_IMAGE_LAYOUT_GENERAL, 1, &depthRegion); - RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); + if (target.stencilAddress == 0) { + VkImageMemoryBarrier discard{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + discard.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + discard.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + discard.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + discard.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + discard.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + discard.image = image; + discard.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1}; + const auto barrier = context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier"); + barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &discard); + context.Function("vkCmdCopyBufferToImage")(commands, depthPlane.staging->Handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &depthRegion); + VkImageMemoryBarrier toGeneral = discard; + toGeneral.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toGeneral.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; + toGeneral.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); + } else { + context.Function("vkCmdCopyBufferToImage")(commands, depthPlane.staging->Handle(), image, VK_IMAGE_LAYOUT_GENERAL, 1, &depthRegion); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); + } if (batch) batch->SubmitAndWait(); - else Recorder::CountBarriers(Recorder::CommandClass::Copy, 3); + else Recorder::CountBarriers(Recorder::CommandClass::Copy, target.stencilAddress == 0 ? 4 : 3); } std::shared_ptr Mirror(const GuestTextureResource& resource) { @@ -1227,8 +1247,10 @@ public: continue; } surface = {}; - const GuestLayer current{resource.baseAddress + geometry.GuestLayerOffset(layer), geometry.layerBytes, resource.tileMode, geometry.thick, GuestMemory::CollectWrites(resource.baseAddress + geometry.GuestLayerOffset(layer), static_cast(geometry.layerBytes))}; - if (held.generation != 0 && held.address == current.address && held.bytes == current.bytes && held.tileMode == current.tileMode && held.thick == current.thick && !Recorder::SnapshotWriteOverlaps(current.address, static_cast(current.bytes)) && GuestMemory::UnchangedSince(current.address, static_cast(current.bytes), held.generation)) { + GuestLayer current{resource.baseAddress + geometry.GuestLayerOffset(layer), geometry.layerBytes, resource.tileMode, geometry.thick, GuestMemory::CollectWrites(resource.baseAddress + geometry.GuestLayerOffset(layer), static_cast(geometry.layerBytes))}; + current.uncommitted = current.generation == 0 && GuestMemory::CommittedRanges(current.address, static_cast(current.bytes)).empty(); + const bool unchanged = current.uncommitted ? held.uncommitted : held.generation != 0 && GuestMemory::UnchangedSince(current.address, static_cast(current.bytes), held.generation); + if (held.address == current.address && held.bytes == current.bytes && held.tileMode == current.tileMode && held.thick == current.thick && !Recorder::SnapshotWriteOverlaps(current.address, static_cast(current.bytes)) && unchanged) { held.generation = current.generation; continue; } @@ -1266,6 +1288,7 @@ private: TextureTileMode tileMode = TextureTileMode::kLinear; bool thick = false; std::uint64_t generation = 0; + bool uncommitted = false; }; VkImage image = VK_NULL_HANDLE; @@ -1970,20 +1993,15 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, const bool created = surface == nullptr; GuestAllocations::Lease creationLease; if (created) { - for (const auto& surface : surfaces()) { - if (!surface->active->load(std::memory_order_acquire) || !surface->samplingActive || surface->context.device != context.device) continue; - const auto bytes = target.samples == 1u ? DepthSliceBytes(target.extent, target.format == VK_FORMAT_D16_UNORM ? 2u : 4u) : MultisampledDepthBytes_nid_no_patch(target.extent, target.format == VK_FORMAT_D16_UNORM ? 2u : 4u, target.samples); - Require(target.address <= std::numeric_limits::max() - bytes && target.htileAddress <= std::numeric_limits::max() - HtileMetadataBytes(target.extent), "native depth attachment range overflows"); - for (const auto& range : surface->sampling.initial) { - Require(target.address >= range.address + range.bytes || range.address >= target.address + bytes, "native multisample depth attachment layout or metadata changed"); - if (target.stencilAddress != 0u) { - const auto stencilBytes = target.samples == 1u ? DepthSliceBytes(target.extent, 1u) : MultisampledDepthBytes_nid_no_patch(target.extent, 1u, target.samples); - Require(target.stencilAddress <= std::numeric_limits::max() - stencilBytes && (target.stencilAddress >= range.address + range.bytes || range.address >= target.stencilAddress + stencilBytes), "native multisample stencil attachment aliases a resident depth surface"); - } - if (target.htileAddress != 0u) Require(target.htileAddress >= range.address + range.bytes || range.address >= target.htileAddress + HtileMetadataBytes(target.extent), "native multisample depth attachment layout or metadata changed"); - } - } + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const auto depthBytes = target.samples == 1u ? DepthSliceBytes(target.extent, d16 ? 2u : 4u) : MultisampledDepthBytes_nid_no_patch(target.extent, d16 ? 2u : 4u, target.samples); + const auto stencilBytes = target.stencilAddress == 0u ? 0u : target.samples == 1u ? DepthSliceBytes(target.extent, 1u) : MultisampledDepthBytes_nid_no_patch(target.extent, 1u, target.samples); + const auto metadataBytes = target.htileAddress == 0u ? 0u : HtileMetadataBytes(target.extent); + Require(target.address <= std::numeric_limits::max() - depthBytes && target.stencilAddress <= std::numeric_limits::max() - stencilBytes && target.htileAddress <= std::numeric_limits::max() - metadataBytes, "native depth attachment range overflows"); lock.unlock(); + RequireNoResidentDepthRead_nid_no_patch(target.address, static_cast(depthBytes)); + if (stencilBytes != 0u) RequireNoResidentDepthRead_nid_no_patch(target.stencilAddress, static_cast(stencilBytes)); + if (metadataBytes != 0u) RequireNoResidentDepthRead_nid_no_patch(target.htileAddress, static_cast(metadataBytes)); auto sampling = target.samples == 1u ? DepthSamplingState{} : prepareDepthSampling(context, target); creationLease = target.samples == 1u || !sampling.refusal.empty() ? GuestAllocations::Lease{} : GuestAllocations::GuestAllocationsAcquireAll_nid_postfix(); lock.lock(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index a91fc9d2ff..f3dd7f8885 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1921,7 +1921,7 @@ struct Kept { // The completion side of a recorded draw: the kept objects, the record check, the lease outcome, // the GPU write notes, the copied-buffer write-back (listed in DrawCopiedWriters or run as a // completion action) and the targets marked dirty. -void keepRecordedDraw(Recorder& recorder, const std::shared_ptr& resources, std::shared_ptr pipeline, std::shared_ptr framebuffer, DrawInputs& inputs, std::vector> targets, std::vector> slices, std::unique_ptr scratch, std::function checkRecords, bool listed, bool completion, const DrawOutcome& outcome) { +void keepRecordedDraw(Recorder& recorder, const std::shared_ptr& resources, std::shared_ptr pipeline, std::shared_ptr framebuffer, DrawInputs& inputs, std::vector> targets, std::vector> slices, VkRect2D scissor, std::unique_ptr scratch, std::function checkRecords, bool listed, bool completion, const DrawOutcome& outcome) { auto kept = std::make_shared(); kept->resources = resources; kept->pipeline = std::move(pipeline); @@ -1961,14 +1961,15 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr if (resources->Reusable()) recorder.OnComplete([resources] { resources->CheckFaults(); }); else recorder.OnComplete([resources] { resources->WriteBackBuffers(); }); } + std::vector storeNow(residents.size(), false); for (std::size_t index = 0; index < residents.size(); ++index) { if (residents[index] == nullptr) continue; - if (index < slices.size()) residents[index]->MarkSliceDirty(slices[index].first, slices[index].second); + if (residents[index]->Descriptor().tileMode == TextureTileMode::kLinear) storeNow[index] = !residents[index]->MarkDrawn(index < slices.size() ? slices[index].first : 0u, index < slices.size() ? slices[index].second : 0u, scissor) || !residents[index]->Cached(); + else if (index < slices.size()) residents[index]->MarkSliceDirty(slices[index].first, slices[index].second); else residents[index]->MarkDirty(); } - for (const auto& resident : residents) { - if (resident == nullptr || resident->Descriptor().tileMode != TextureTileMode::kLinear) continue; - StorageTexture::FlushPending(resident->Descriptor().baseAddress, static_cast(resident->GuestBytes()), nullptr, "linear target"); + for (std::size_t index = 0; index < residents.size(); ++index) { + if (storeNow[index]) StorageTexture::FlushPending(residents[index]->Descriptor().baseAddress, static_cast(residents[index]->GuestBytes()), nullptr, "linear target"); } } @@ -2195,7 +2196,6 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam recorder->FlushStores(); outcome.storesFlushed = true; } - const auto drawBindings = resources.PrepareDrawBindings(*recorder, record.moved); const bool capture = CaptureInputsEnabled(); const bool meshIndirect = state.stages.mesh && args != nullptr; const bool addressBased = resources.HoldsLease(); @@ -2217,6 +2217,8 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam } for (const auto image : record.multisampleImages) passAttachments.emplace_back(image, true); const bool unknownMemory = addressBased || resources.UsesAddressTable() || resources.HasCopiedWrites(); + static_cast(StorageTexture::StoreDrawnLinearTouching("draw access", [&](std::uint64_t begin, std::uint64_t end) { return std::any_of(passReads.begin(), passReads.end(), [&](const auto& read) { return read.first < end && begin < read.second; }) || resources.WritesOverlap(begin, static_cast(end - begin)); }, unknownMemory)); + const auto drawBindings = resources.PrepareDrawBindings(*recorder, record.moved); static const bool addressReadPasses = std::getenv("APS5_NO_ADDRESS_READ_PASSES") == nullptr; const bool readOnlyBda = addressReadPasses && resources.BdaReadOnly(); outcome.addressDraw = addressBased; @@ -2361,7 +2363,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam } recorder->LeaveRenderPassOpen(passKey, drawTiming, fenced, passAccess, std::move(storeProxies)); timer.phase(PhaseRecord); - keepRecordedDraw(*recorder, record.resources, std::move(record.pipeline), std::move(record.framebuffer), inputs, std::move(record.targets), std::move(record.targetSlices), std::move(scratch), std::move(checkRecords), record.listed, record.completion, outcome); + keepRecordedDraw(*recorder, record.resources, std::move(record.pipeline), std::move(record.framebuffer), inputs, std::move(record.targets), std::move(record.targetSlices), state.scissor, std::move(scratch), std::move(checkRecords), record.listed, record.completion, outcome); timer.phase(PhaseKeep); if (record.waited) { // The wait the dispatch path makes for such work (source 3, "address-based"): the @@ -2952,6 +2954,14 @@ void recordPreparedDraw(const Context& context, const State& state, const Pm4::D report(outcome.waited ? (outcome.reason == SyncLease ? " recorded then waited (lease)" : " recorded then waited (copied writes)") : completion ? " recorded with completion" : " recorded"); return; } + const bool gpuIndirect = args != nullptr && indirect.path == IndirectDrawPath::Gpu; + const auto touches = [&](std::uint64_t begin, std::uint64_t end) { + const auto bytes = static_cast(end - begin); + if (resources->WritesOverlap(begin, bytes) || resources->ReadsOverlap(begin, bytes)) return true; + if (!gpuIndirect) return false; + return (begin < args->arguments + args->RangeBytes() && args->arguments < end) || (args->countIndirect && begin < args->countAddress + 4 && args->countAddress < end); + }; + static_cast(StorageTexture::StoreDrawnLinearTouching("draw access", [&](std::uint64_t begin, std::uint64_t end) { return touches(begin, end) || std::any_of(inputs.inPlaceReads.begin(), inputs.inPlaceReads.end(), [&](const auto& read) { return read.first < end && begin < read.second; }); }, resources->HoldsLease() || resources->UsesAddressTable() || resources->HasCopiedWrites())); APS5_LOG_CHARS_OUT_DEBUG("Creating CommandBatch"); std::optional batch; if (!recorded) { @@ -2961,16 +2971,9 @@ void recordPreparedDraw(const Context& context, const State& state, const Pm4::D } using CommandClass = Recorder::CommandClass; const auto countBarrier = [&](std::uint32_t count = 1) { if (recorded) Recorder::CountBarriers(CommandClass::Draw, count); }; - const bool gpuIndirect = args != nullptr && indirect.path == IndirectDrawPath::Gpu; // A queued DCC key store over memory the draw writes or reads in place (unknown for an // address-based build), or over its GPU-side records, lands before it, as before a dispatch // (VulkanDevice::dispatch). - const auto touches = [&](std::uint64_t begin, std::uint64_t end) { - const auto bytes = static_cast(end - begin); - if (resources->WritesOverlap(begin, bytes) || resources->ReadsOverlap(begin, bytes)) return true; - if (!gpuIndirect) return false; - return (begin < args->arguments + args->RangeBytes() && args->arguments < end) || (args->countIndirect && begin < args->countAddress + 4 && args->countAddress < end); - }; if (recorded && recorder->HasQueuedKeyStores() && (resources->HoldsLease() || recorder->AnyQueuedKeyStore(touches))) recorder->FlushKeyStores(); // A queued label store over such memory likewise (Recorder::RecordStore). if (recorded && recorder->HasQueuedStores() && (resources->HoldsLease() || recorder->AnyQueuedStore(touches))) recorder->FlushStores(); @@ -3137,7 +3140,7 @@ void recordPreparedDraw(const Context& context, const State& state, const Pm4::D APS5_LOG_CHARS_OUT_DEBUG("SubmitAndWait"); timer.phase(PhaseRecord); if (recorded) { - keepRecordedDraw(*recorder, resources, pipeline, framebuffer, inputs, owners, [&] { std::vector> slices; for (const auto& binding : targets) slices.emplace_back(binding.color.arraySlice, binding.color.arrayLastSlice != 0 ? binding.color.layers : 1u); return slices; }(), std::move(scratch), std::move(checkRecords), listed, completion, outcome); + keepRecordedDraw(*recorder, resources, pipeline, framebuffer, inputs, owners, [&] { std::vector> slices; for (const auto& binding : targets) slices.emplace_back(binding.color.arraySlice, binding.color.arrayLastSlice != 0 ? binding.color.layers : 1u); return slices; }(), state.scissor, std::move(scratch), std::move(checkRecords), listed, completion, outcome); timer.phase(PhaseKeep); if (outcome.waited) { // The wait the dispatch path makes for such work (source 3, "address-based"): the @@ -3279,7 +3282,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con timer.phase(PhaseSetup); if (state.colors[index].fmaskAddress != 0) GuardOneFragmentFmask(context, state.colors[index]); auto stored = recipe.targets[index].lock(); - if (stored == nullptr || !StorageImageCached(context, stored.get()) || !StorageImageServesKeys(*stored, state.colors[index].dccAddress)) return miss(DrawRecipeMiss::TargetGone); + if (stored == nullptr || !StorageImageCached(context, stored.get()) || !StorageImageServesKeys(*stored, SurfaceForTarget(state.colors[index]).dccAddress)) return miss(DrawRecipeMiss::TargetGone); auto resident = refreshResidentTarget(context, state, state.colors[index], outcome, profile, [&] { if (state.colors[index].arrayLastSlice != 0) stored->RefreshLayers(state.colors[index].arraySlice, 1); else stored->Refresh(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 12939ea058..fcd09869de 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -69,6 +69,7 @@ struct ImageMirror { std::uint64_t bytes = 0; bool writable = false; bool heap = false; + bool joinedHeap = false; bool sparse = false; std::uint64_t reserved = 0; std::vector> committed; @@ -285,6 +286,7 @@ struct HostImports { bool allocationFailed = false; std::uint8_t reportNoted = 0; std::map spaceUses; + std::uint64_t budgetRefusals = 0; }; HostImports& Imports() { @@ -868,6 +870,7 @@ bool importsStale(const Context& context, const HostImports& state); constexpr std::uint64_t settleFloorBytes = 0x100000; #ifdef _WIN32 constexpr std::uint64_t settleBlockBytes = 0x10000; +constexpr std::uint64_t settlePageBytes = 0x1000; constexpr auto settlePollPeriod = std::chrono::microseconds(100); std::atomic deferredSettles{std::getenv("APS5_NO_DEFERRED_SETTLES") == nullptr}; #endif @@ -1094,8 +1097,22 @@ void reapSettleBatches(const Context& context, HostImports& state) { } } +bool comparedImportShares(const HostImports& state, std::uint64_t own, std::uint64_t begin, std::uint64_t end) { + const auto shares = [&](const HostImport& entry) { return entry.unwatched && entry.memoryBase < end && begin < entry.memoryBase + entry.pinnedBytes; }; + for (const auto& [base, entry] : state.imports) { + if (base != own && shares(entry)) return true; + } + for (const auto& [device, list] : state.parked) { + for (const auto& retired : list) { + if (shares(retired->node.mapped())) return true; + } + } + return false; +} + void rewatchSettledImports(HostImports& state) { if (state.settleReady.empty()) return; + std::vector bases; std::vector> ranges; for (const auto base : state.settleReady) { const auto settle = state.pendingSettles.find(base); @@ -1103,19 +1120,23 @@ void rewatchSettledImports(HostImports& state) { state.pendingSettles.erase(settle); const auto import = state.imports.find(base); if (import == state.imports.end() || !import->second.unwatched) continue; - const std::array, 1> span{{{base, base + import->second.bytes}}}; + const auto first = base & ~(settleBlockBytes - 1); + const auto stop = (base + import->second.bytes + settleBlockBytes - 1) & ~(settleBlockBytes - 1); + if ((first != base || stop != base + import->second.bytes) && comparedImportShares(state, base, first, stop)) continue; + const std::array, 1> span{{{first, stop}}}; if (StorageTexture::AnyLiveOverlaps(span)) { ++state.settlesKeptCompared; continue; } - ranges.push_back(span.front()); + bases.push_back(base); + ranges.emplace_back(base & ~(settlePageBytes - 1), (base + import->second.bytes + settlePageBytes - 1) & ~(settlePageBytes - 1)); } state.settleReady.clear(); if (ranges.empty()) return; GuestMemory::Rewatch(ranges); - for (const auto& [begin, end] : ranges) { - const auto import = state.imports.find(begin); - if (import == state.imports.end() || !GuestMemory::Watched(begin, static_cast(end - begin))) continue; + for (const auto base : bases) { + const auto import = state.imports.find(base); + if (import == state.imports.end() || !GuestMemory::Watched(base, static_cast(import->second.bytes))) continue; import->second.unwatched = false; ++state.settlesRewatched; } @@ -1130,6 +1151,49 @@ void settleUnwatchedImports(const Context& context, HostImports& state) { rewatchSettledImports(state); submitSettleBatch(context, state); } + +void settleImportsNow(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end) { + if (context.settleQueue == VK_NULL_HANDLE || context.settleQueueMutex == nullptr || !GuestMemory::GpuMutex().HeldByThisThread()) return; + std::vector needed; + auto it = state.imports.lower_bound(begin); + if (it != state.imports.begin()) --it; + for (; it != state.imports.end() && it->first < end; ++it) { + auto& entry = it->second; + if (entry.base + entry.bytes <= begin || !entry.unwatched) continue; + auto settle = state.pendingSettles.find(it->first); + if (settle == state.pendingSettles.end()) { + if (!entry.settleOnDemand || !deferredSettles.load(std::memory_order_relaxed)) continue; + entry.settleOnDemand = false; + settle = state.pendingSettles.insert_or_assign(it->first, PendingSettle{}).first; + } + if (settle->second.phase == SettlePhase::Wanted) { + std::erase(state.settleWanted, it->first); + state.settleWanted.insert(state.settleWanted.begin(), it->first); + } + needed.push_back(it->first); + } + if (needed.empty()) return; + auto& deviceSettle = settleFor(context, state); + const auto wanted = [&] { + return std::any_of(needed.begin(), needed.end(), [&](std::uint64_t base) { + const auto settle = state.pendingSettles.find(base); + return settle != state.pendingSettles.end() && settle->second.phase == SettlePhase::Wanted; + }); + }; + while (wanted()) { + if (std::all_of(deviceSettle.batches.begin(), deviceSettle.batches.end(), [](const SettleBatch& batch) { return batch.busy; })) { + for (const auto& batch : deviceSettle.batches) waitSettleBatch(deviceSettle, context.device, batch); + reapSettleBatches(context, state); + } + submitSettleBatch(context, state); + } + for (const auto base : needed) { + const auto settle = state.pendingSettles.find(base); + if (settle != state.pendingSettles.end() && settle->second.phase == SettlePhase::Submitted && settle->second.device == context.device) waitSettleBatch(deviceSettle, context.device, deviceSettle.batches[settle->second.batch]); + } + reapSettleBatches(context, state); + rewatchSettledImports(state); +} #endif void decideImportWatch(const Context& context, HostImports& state) { @@ -1429,8 +1493,32 @@ bool evictingRefusalCurrent(const HostImports& state, const EvictingRefusal& ref return refusal.epoch == state.epoch && refusal.liveBytes == state.liveBytes && budget <= refusal.budget && refusal.window == importWindow.load(std::memory_order_relaxed) && state.useCounter < refusal.ready && (state.buildFloor <= refusal.floor || state.useCounter < refusal.readyHeld); } +void noteRetiredUnwatched(std::vector>& spans, const HostImport& entry, const GuestAllocations::Lease& lease) { + if (!entry.unwatched) return; + for (const auto& range : entry.ranges) { + if (leasedRangeOwner(lease, range->address) != range) continue; + const auto begin = std::max(range->address, entry.base); + const auto end = std::min(range->address + range->bytes, entry.base + entry.bytes); + if (begin < end) spans.emplace_back(begin, end); + } +} + +void rewatchRetired(const HostImports& state, std::vector> spans) { + std::erase_if(spans, [&](const auto& span) { + auto it = state.imports.upper_bound(span.first); + if (it != state.imports.begin()) --it; + for (; it != state.imports.end() && it->first < span.second; ++it) { + if (it->second.unwatched && span.first < it->first + it->second.bytes) return true; + } + const std::array, 1> one{{span}}; + return StorageTexture::AnyLiveOverlaps(one); + }); + if (!spans.empty()) GuestMemory::Rewatch(spans); +} + std::uint64_t retireLeastRecent(const Context& context, HostImports& state, const GuestAllocations::Lease& lease, std::uint64_t wanted) { std::uint64_t freed = 0; + std::vector> unwatched; while (freed < wanted) { auto oldest = state.imports.end(); for (auto it = state.imports.begin(); it != state.imports.end(); ++it) { @@ -1438,8 +1526,10 @@ std::uint64_t retireLeastRecent(const Context& context, HostImports& state, cons } if (oldest == state.imports.end()) break; freed += oldest->second.pinnedBytes; + noteRetiredUnwatched(unwatched, oldest->second, lease); retireImport(context.device, state, oldest, lease); } + rewatchRetired(state, std::move(unwatched)); return freed; } @@ -1631,6 +1721,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s ++counts.refused; counts.refusedBytes += lease[index]->bytes; ++counts.remembered; + ++state.budgetRefusals; return nullptr; } const auto bounds = importRunBounds(lease, address, largest, budget); @@ -1796,6 +1887,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import if (evict) state.evictingRefusals.insert_or_assign(std::pair{base, bytes}, EvictingRefusal{base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end()), state.epoch, state.liveBytes, budget, room, importWindow.load(std::memory_order_relaxed), state.buildFloor, ready, readyHeld, joined}); if (stepDown && containing != run.ranges.end() && room >= (*containing)->bytes) return importAllocation(context, state, address, lease, requester, end, evict, std::min(bytes / 2, room)); if (containing != run.ranges.end() && run.ranges.size() > 1) remember((*containing)->address, (*containing)->bytes, GuestAllocations::Mapped{*containing}); + ++state.budgetRefusals; return nullptr; } for (auto it = first; it != state.imports.end() && it->first < base + bytes;) { @@ -1884,7 +1976,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import GuestMemory::ImportWatched(base, bytes, [&] { step = createBackingImport(context, entry, backing, result); return step == nullptr; - }, watchedDmaBuf); + }, watchedDmaBuf, false); } if (step == nullptr && entry.dmaBuf && bytes >= settleFloorBytes && context.settleQueue != VK_NULL_HANDLE && context.settleQueueMutex != nullptr) { static const bool traceSettle = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr; @@ -1948,8 +2040,10 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import entry.joined = joined; entry.lastUse = ++state.useCounter; #ifdef _WIN32 - if (deferredSettles.load(std::memory_order_relaxed) && entry.alias == nullptr && state.watchPrivateImports && writable && base % settleBlockBytes == 0 && bytes % settleBlockBytes == 0) { - if (entry.unwatched) { + if (deferredSettles.load(std::memory_order_relaxed) && entry.alias == nullptr && state.watchPrivateImports && writable) { + if (base % settleBlockBytes != 0 || bytes % settleBlockBytes != 0) { + entry.settleOnDemand = entry.unwatched; + } else if (entry.unwatched) { state.pendingSettles.insert_or_assign(base, PendingSettle{}); state.settleWanted.push_back(base); } else if (settled && !GuestMemory::Watched(base, static_cast(bytes))) { @@ -2020,6 +2114,7 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc std::erase_if(state.sparseRunEnds, [&](const auto& entry) { return !starts(entry.first); }); std::erase_if(state.sparseRunJoins, [&](const auto& entry) { return !starts(entry.first); }); const auto budget = effectiveBudget(context, state); + std::vector> unwatched; for (auto it = state.imports.begin(); it != state.imports.end();) { if (sameRange(it->second, lease, budget)) { if (it->second.unwatched) GuestMemory::Unwatch(it->first, it->second.bytes); @@ -2029,9 +2124,11 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr; if (trace && leasedRangeOwner(lease, it->first, it->first + it->second.bytes) != nullptr) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx retired: the range was mapped again\n", static_cast(it->first), static_cast(it->second.bytes)); const auto next = std::next(it); + noteRetiredUnwatched(unwatched, it->second, lease); retireImport(context.device, state, it, lease); it = next; } + rewatchRetired(state, std::move(unwatched)); for (auto it = state.failed.begin(); it != state.failed.end();) it = sameRange(it->second, lease, budget) ? std::next(it) : state.failed.erase(it); for (auto it = state.failedJoins.begin(); it != state.failedJoins.end();) it = sameRun(it->second, joinedRun(lease, it->second.base, it->second.base + it->second.bytes)) ? std::next(it) : state.failedJoins.erase(it); for (auto it = state.failedBlocks.begin(); it != state.failedBlocks.end();) it = sameRun(it->second, exactRun(lease, it->first.first, it->first.second)) ? std::next(it) : state.failedBlocks.erase(it); @@ -2202,6 +2299,39 @@ void unlistMirror(ImageMirrors& state, ImageMirror& mirror) { mirror.listed = false; } +struct JoinedHeapRange { + std::vector> members; + std::shared_ptr range; +}; + +struct JoinedHeapRanges { + std::mutex mutex; + std::map entries; +}; + +JoinedHeapRanges& JoinedHeaps() { + static JoinedHeapRanges ranges; + return ranges; +} + +bool releaseExpiredJoinedHeaps() { + std::vector> released; + { + auto& joined = JoinedHeaps(); + std::lock_guard lock(joined.mutex); + for (auto it = joined.entries.begin(); it != joined.entries.end();) { + const auto& members = it->second.members; + if (std::none_of(members.begin(), members.end(), [](const auto& member) { return member.expired(); })) { + ++it; + continue; + } + released.push_back(std::move(it->second.range)); + it = joined.entries.erase(it); + } + } + return !released.empty(); +} + std::uint64_t blockBytes(const GuestRuns& runs) { constexpr std::uint64_t block = ResidentBuffer::BlockBytes; std::uint64_t blocks = 0; @@ -2708,10 +2838,15 @@ bool importsNow(const Context& context, HostImports& state, const GuestAllocatio return context.hostImportAlignment != 0 && (importsStale(context, state) || importAllocation(context, state, address, lease, ImportRequester::Build, 0, false) != nullptr); } +const char* heapCopyReason(bool overBudget, bool watched) { + if (overBudget) return watched ? "import over the host import budget, heap mirror refused" : "import over the host import budget, heap not fully write-watched"; + return watched ? "heap mirror refused" : "heap not fully write-watched"; +} + // The mirror for a leased image range: the existing one, or a new one when none exists or the // registered Range object changed (a protection change). Null when the range cannot be mirrored. An // existing writable mirror's blocks are appended to `blocks` for the caller's one compare of every -std::shared_ptr acquireMirror(const Context& context, const std::shared_ptr& range, std::vector& blocks, bool heap, std::uint64_t begin = 0, std::uint64_t bytes = 0) { +std::shared_ptr acquireMirror(const Context& context, const std::shared_ptr& range, std::vector& blocks, bool heap, std::uint64_t begin = 0, std::uint64_t bytes = 0, bool joinedHeap = false) { auto selected = *range; if (bytes != 0) { Require(begin >= range->address && bytes <= range->bytes && begin - range->address <= range->bytes - bytes, "heap mirror selection exceeds its registered range"); @@ -2732,7 +2867,7 @@ std::shared_ptr acquireMirror(const Context& context, const std::sh } if (state.failed.contains(selected.address)) return nullptr; const auto found = state.entries.find(selected.address); - if (found != state.entries.end() && sameRange(found->second->range, range) && found->second->reserved == selected.bytes && found->second->writable == range->writable && found->second->heap == heap) mirror = found->second; + if (found != state.entries.end() && sameRange(found->second->range, range) && found->second->reserved == selected.bytes && found->second->writable == range->writable && found->second->heap == heap && found->second->joinedHeap == joinedHeap) mirror = found->second; } if (mirror != nullptr) { if (!mirror->sparse) { @@ -2755,6 +2890,7 @@ std::shared_ptr acquireMirror(const Context& context, const std::sh mirror->sparse = sparse; mirror->writable = range->writable; mirror->heap = heap; + mirror->joinedHeap = joinedHeap; mirror->range = range; GuestRuns backed; if (heap) { @@ -2887,9 +3023,11 @@ std::shared_ptr findMirror(const Context& context, std::uint64_t be // Drops mirrors whose registered range is gone (a later build made a new one for the replacement). void sweepMirrors() { + const bool released = releaseExpiredJoinedHeaps(); const auto space = Spaces().current.load(); auto& state = Mirrors(); std::lock_guard lock(state.mutex); + if (released) ++state.version; if (space != nullptr && state.pinnedSpace == space->serial && state.pinnedVersion == state.version) return; ++state.sweeps; bool pinned = space != nullptr; @@ -2913,6 +3051,14 @@ void sweepMirrors() { void releaseImportedMirrors(const Context& context, std::span> imported) { if (imported.empty()) return; + std::vector> coverage(imported.begin(), imported.end()); + std::sort(coverage.begin(), coverage.end()); + std::size_t count = 0; + for (const auto& interval : coverage) { + if (count != 0 && interval.first <= coverage[count - 1].second) coverage[count - 1].second = std::max(coverage[count - 1].second, interval.second); + else coverage[count++] = interval; + } + coverage.resize(count); std::vector> released; auto& state = Mirrors(); std::lock_guard lock(state.mutex); @@ -2926,6 +3072,13 @@ void releaseImportedMirrors(const Context& context, std::spanjoinedHeap) { + const auto covered = std::upper_bound(coverage.begin(), coverage.end(), mirror->base, [](std::uint64_t address, const auto& interval) { return address < interval.first; }); + if (covered == coverage.begin() || std::prev(covered)->second < mirror->base + mirror->reserved) { + ++it; + continue; + } + } ++state.heapImported; state.heapImportedBytes += mirror->accounted; unlistMirror(state, *mirror); @@ -3235,7 +3388,8 @@ std::string AddressCopyOverflow(std::vector copies, std::uint64_t l std::string message = text; for (std::size_t index = 0; index < copies.size() && index < 8; ++index) { const auto& copy = copies[index]; - std::snprintf(text, sizeof(text), " 0x%llx+0x%llx (%.1f MiB committed, %s)", static_cast(copy.begin), static_cast(copy.end - copy.begin), copy.committed / 1048576.0, copy.reason); + const char* reason = copy.heap ? heapCopyReason(copy.overBudget, GuestMemory::Watched(copy.begin, copy.end - copy.begin)) : copy.reason; + std::snprintf(text, sizeof(text), " 0x%llx+0x%llx (%.1f MiB committed, %s)", static_cast(copy.begin), static_cast(copy.end - copy.begin), copy.committed / 1048576.0, reason); message += text; } return message; @@ -3716,6 +3870,22 @@ const HostImport* ExistingHostImportFor(const Context& context, std::uint64_t ad return findImport(state, address, address + bytes); } +bool WatchedAfterSettle(const Context& context, std::uint64_t address, std::size_t bytes) { +#ifdef _WIN32 + if (context.hostImportAlignment != 0 && bytes != 0 && bytes <= std::numeric_limits::max() - address && GuestMemory::GpuMutex().HeldByThisThread()) { + ensurePinWaiter(); + auto& state = Imports(); + std::lock_guard lock(state.mutex); + if (importsStale(context, state)) { + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + refreshImports(context, state, lease); + } + settleImportsNow(context, state, address, address + bytes); + } +#endif + return GuestMemory::Watched(address, bytes); +} + bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice) { if (context.hostImportAlignment == 0) return true; ensurePinWaiter(); @@ -3780,6 +3950,21 @@ bool HostImportCovers(const Context& context, std::uint64_t address, std::size_t return state.device == context.device && peekImport(state, address, address + bytes) != nullptr; } +bool ComparedHostImportCovers(const Context& context, std::uint64_t address, std::size_t bytes) { +#ifdef _WIN32 + static_cast(context); + static_cast(address); + static_cast(bytes); + return false; +#else + if (context.hostImportAlignment == 0 || bytes == 0 || bytes > std::numeric_limits::max() - address) return false; + auto& state = Imports(); + std::lock_guard lock(state.mutex); + const auto* import = state.device == context.device ? peekImport(state, address, address + bytes) : nullptr; + return import != nullptr && import->unwatched; +#endif +} + GuestBufferMemory::GuestBufferMemory(const Context& context) : context(context) {} bool GuestBufferMemory::WritesOverlap(std::uint64_t address, std::size_t bytes) const { @@ -3925,16 +4110,24 @@ void GuestBufferMemory::AcquireRegistered() { // The import lock is taken per range: a mirror's preparation may wait for recorded work, which // must not happen under it. regions.reserve(lease.size()); + static const std::uint64_t copyLimit = [] { + const char* value = std::getenv("APS5_ADDRESS_COPY_MAX_MIB"); + return (value ? std::strtoull(value, nullptr, 10) : 64ull) << 20u; + }(); + const auto copyOverride = addressCopyLimitOverride.load(std::memory_order_relaxed); + const auto cap = copyOverride != 0 ? copyOverride : copyLimit; std::vector copies; std::vector> imported; const HostImport* covering = nullptr; std::uint64_t coveredBegin = 0; std::uint64_t coveredEnd = 0; + std::uint64_t copiedBytes = 0; for (const auto& range : lease) { if (!range->readable || !range->gpu) continue; validate(range->address, range->bytes); Region region{range->address, range->address + range->bytes, range->writable, {}, nullptr}; const HostImport* entry = nullptr; + bool overBudget = false; std::vector pieces; const auto limit = importMaxBytes.load(std::memory_order_relaxed); if (importable && covering != nullptr && range->address >= coveredBegin && range->address + range->bytes <= coveredEnd) { @@ -3946,7 +4139,9 @@ void GuestBufferMemory::AcquireRegistered() { } else if (importable) { auto& state = Imports(); std::lock_guard lock(state.mutex); + const auto refusals = state.budgetRefusals; entry = importAllocation(context, state, range->address, lease, ImportRequester::Build, 0, false); + overBudget = state.budgetRefusals != refusals; covering = entry; if (entry != nullptr) { coveredBegin = entry->base; @@ -4012,17 +4207,36 @@ void GuestBufferMemory::AcquireRegistered() { committed = 0; for (const auto& [first, last] : copied.backed) committed += last - first; } - const char* reason = !mirrorsEnabled() ? "mirrors disabled by APS5_NO_LEASE_MIRROR" : copied.sparse ? "unreadable pages" : !range->releasable ? "image mirror refused" : "outside the write-watched arena"; - copies.push_back({copied.begin, copied.end, committed, reason}); + if (overBudget && copiedBytes + committed > cap) { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + entry = importAllocation(context, state, range->address, lease, ImportRequester::Build, 0, true); + if (entry != nullptr) { + covering = entry; + coveredBegin = entry->base; + coveredEnd = entry->base + entry->bytes; + } + static auto lastReport = std::chrono::steady_clock::now() - std::chrono::seconds(60); + if (entry != nullptr && std::chrono::steady_clock::now() - lastReport > std::chrono::seconds(10)) { + lastReport = std::chrono::steady_clock::now(); + std::fprintf(stderr, "[gpu] host import budget: 0x%llx+0x%llx imported by evicting idle imports, as copying it would pass APS5_ADDRESS_COPY_MAX_MIB\n", static_cast(range->address), static_cast(range->bytes)); + } + } + if (entry != nullptr) { + regions.pop_back(); + region.hostBacked = true; + region.direct = entry; + regions.push_back(std::move(region)); + imported.emplace_back(range->address, range->address + range->bytes); + continue; + } + copiedBytes += committed; + const char* reason = !mirrorsEnabled() ? "mirrors disabled by APS5_NO_LEASE_MIRROR" : copied.sparse ? "unreadable pages" : !range->releasable ? "image mirror refused" : nullptr; + copies.push_back({copied.begin, copied.end, committed, reason, reason == nullptr, overBudget}); } releaseImportedMirrors(context, imported); regionsSorted = sortedLookup; - static const std::uint64_t copyLimit = [] { - const char* value = std::getenv("APS5_ADDRESS_COPY_MAX_MIB"); - return (value ? std::strtoull(value, nullptr, 10) : 64ull) << 20u; - }(); - const auto limit = addressCopyLimitOverride.load(std::memory_order_relaxed); - if (const auto overflow = AddressCopyOverflow(std::move(copies), limit != 0 ? limit : copyLimit); !overflow.empty()) { + if (const auto overflow = AddressCopyOverflow(std::move(copies), cap); !overflow.empty()) { std::fprintf(stderr, "FATAL: %s\n", overflow.c_str()); std::fflush(stderr); std::abort(); @@ -4270,15 +4484,40 @@ bool GuestBufferMemory::sparseHeapCandidate(const Region& region) { return region.sparseHeap && region.sparse && region.mirror == nullptr && region.direct == nullptr && (region.hostBacked || region.writable) && mirrorsEnabled() && sparseMirrorsEnabled(); } +std::shared_ptr joinedHeapRange(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { + const auto run = crossedRun(lease, begin, end); + if (run.ranges.size() < 2 || std::any_of(run.ranges.begin(), run.ranges.end(), [](const auto& range) { return !range->releasable; })) return nullptr; + releaseExpiredJoinedHeaps(); + auto& joined = JoinedHeaps(); + std::lock_guard lock(joined.mutex); + auto& entry = joined.entries[run.base]; + bool same = entry.range != nullptr && entry.members.size() == run.ranges.size(); + for (std::size_t index = 0; same && index < run.ranges.size(); ++index) same = sameRange(entry.members[index], run.ranges[index]); + if (!same) { + const auto& first = *run.ranges.front(); + entry.members.assign(run.ranges.begin(), run.ranges.end()); + entry.range = std::make_shared(GuestAllocations::Range{run.base, static_cast(run.bytes), true, first.writable, first.allocationAddress, first.allocationBytes, true, first.gpu, first.backingSerial}); + } + return entry.range; +} + bool GuestBufferMemory::bindSparseMirror(Region& region, const GuestAllocations::Lease& lease) { const auto backedEnd = region.backed.empty() ? region.begin : region.backed.back().second; const auto neededEnd = std::min(region.end, std::max(backedEnd, std::max(region.lastView, region.begin) + 4u)); - const auto* range = containingRange(lease, region.begin, neededEnd); - if (range == nullptr || !range->releasable) return false; + std::shared_ptr owner; + bool joined = false; + if (const auto* range = containingRange(lease, region.begin, neededEnd); range != nullptr) { + if (!range->releasable) return false; + owner = leasedRangeOwner(lease, range->address); + } else { + joined = true; + owner = joinedHeapRange(lease, region.begin, neededEnd); + if (owner == nullptr) return false; + } std::vector blocks; std::shared_ptr mirror; try { - mirror = acquireMirror(context, leasedRangeOwner(lease, range->address), blocks, true); + mirror = acquireMirror(context, owner, blocks, true, 0, 0, joined); } catch (const std::runtime_error& error) { static std::atomic reported{0}; if (reported.fetch_add(1, std::memory_order_relaxed) < 4) std::fprintf(stderr, "[gpu] %s; copying the sparse heap instead\n", error.what()); @@ -4462,6 +4701,7 @@ VkBufferUsageFlags gpuCopyUsage(bool addressable) { // window takes those elements there. APS5_ATOMIC_STAGE_MAX_KIB (1024) bounds the atomic one. constexpr std::uint64_t SweptShadowMax = std::uint64_t{16} << 20u; constexpr std::uint64_t DiscreteWrittenShadowMax = std::uint64_t{8} << 20u; +constexpr std::uint64_t DiscreteSweptShadowMax = std::uint64_t{256} << 20u; bool writtenShadowEnabled() { static const bool disabled = std::getenv("APS5_NO_WRITTEN_SHADOW") != nullptr; @@ -4604,7 +4844,7 @@ bool GuestBufferMemory::stagingEligible(const Region& region, bool addressable) const auto bytes = region.end - region.begin; if (!WritesOverlap(region.begin, static_cast(bytes))) return false; if (region.atomic && atomicStagingEnabled() && bytes <= atomicStageMax()) return true; - return writtenShadowEnabled() && bytes >= writtenShadowMin() && bytes <= (region.swept ? std::max(writtenShadowMax(context.discreteGpu), SweptShadowMax) : writtenShadowMax(context.discreteGpu)); + return writtenShadowEnabled() && bytes >= writtenShadowMin() && bytes <= (region.swept ? std::max(writtenShadowMax(context.discreteGpu), context.discreteGpu ? DiscreteSweptShadowMax : SweptShadowMax) : writtenShadowMax(context.discreteGpu)); } bool GuestBufferMemory::overlayCandidate(const Region& inside, bool readOnly) const { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 0520466262..7cb1eafdad 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/FirstSampleCopy.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" @@ -331,7 +332,9 @@ public: colorFill = MultisampledColorFill(use); fromTexture = false; dccRanges.clear(); + dccCopies.clear(); fmaskRange.reset(); + fmaskCopy = {}; metadataValid = std::make_shared>(true); const auto initial = initialDccClear(use); recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { @@ -500,6 +503,7 @@ public: Context context; std::shared_ptr> active = std::make_shared>(true); + std::shared_ptr> hostOnly = std::make_shared>(true); std::shared_ptr> metadataValid; ColorTarget target; VkImage image = VK_NULL_HANDLE; @@ -533,9 +537,35 @@ private: std::unique_ptr firstSampleCopy; std::map, std::shared_ptr> firstSampleTextures; std::vector dccRanges; + struct DccCopy { + bool compared = false; + std::vector bytes; + std::uint64_t stamp = 0; + std::array checked{}; + }; + mutable std::vector dccCopies; + mutable DccCopy fmaskCopy; std::optional dccFill; std::optional cmaskFill; + bool compared(std::size_t index) const { + return index < dccCopies.size() && dccCopies[index].compared; + } + + static DccCopy comparedCopy(const GuestMemory::UnchangedQuery& range, std::vector bytes) { + return {true, std::move(bytes), GuestMemory::NewestStamp(range.address, range.bytes), {}}; + } + + static bool copyCurrent(const GuestMemory::UnchangedQuery& range, DccCopy& copy) { + if (GuestMemory::NewestStamp(range.address, range.bytes) != copy.stamp) return false; + const auto epoch = GuestMemory::CollectEpoch(); + const std::array seen{epoch, GuestMemory::ForgetSerial()}; + if (epoch != 0 && (seen[1] & 1u) == 0 && copy.checked == seen) return true; + if (!GuestMemory::EqualsCommittedUnsynced(range.address, copy.bytes)) return false; + copy.checked = seen; + return true; + } + bool absorbDccFill(const ColorTarget& use, VkClearColorValue& clear) { const auto dcc = std::exchange(dccFill, std::nullopt); const auto cmaskPattern = std::exchange(cmaskFill, std::nullopt); @@ -546,19 +576,30 @@ private: if (dccRanges.size() > 2) { const auto& range = dccRanges[2]; auto* recorder = Recorder::Active(); - const bool written = !GuestMemory::UnchangedSince(range.address, range.bytes, range.generation) || (recorder != nullptr && recorder->PendingWriteOverlaps(range.address, range.bytes)); + const bool unchanged = compared(2) ? copyCurrent(range, dccCopies[2]) : GuestMemory::UnchangedSince(range.address, range.bytes, range.generation); + const bool written = !unchanged || (recorder != nullptr && recorder->PendingWriteOverlaps(range.address, range.bytes)); const auto landed = LandedUniformMetadataByte(range.address, range.bytes); if (!landed || (written && (!cmaskPattern || *cmaskPattern != *landed * 0x01010101u))) return false; cmaskKeys = *landed; } std::vector metadata(dccRanges.begin() + 1, dccRanges.end()); - for (auto& range : metadata) { - if (!GuestMemory::Watched(range.address, range.bytes)) return false; + std::vector comparedMetadata(metadata.size(), false); + for (std::size_t index = 0; index < metadata.size(); ++index) { + auto& range = metadata[index]; + if (!GuestMemory::Watched(range.address, range.bytes)) { + if (!compared(index + 1) && !ComparedHostImportCovers(context, range.address, range.bytes)) return false; + comparedMetadata[index] = true; + range.generation = 0; + continue; + } range.generation = collectBlocks(range.address, range.bytes); if (range.generation == 0) return false; } - clear = MultisampledDccClear(use, std::vector(dccRanges[1].bytes, std::byte{*keys}), std::vector(use.cmaskBytes, std::byte{cmaskKeys})); + std::array, 2> landedBytes{std::vector(dccRanges[1].bytes, std::byte{*keys}), std::vector(use.cmaskBytes, std::byte{cmaskKeys})}; + clear = MultisampledDccClear(use, landedBytes[0], landedBytes[1]); std::copy(metadata.begin(), metadata.end(), dccRanges.begin() + 1); + dccCopies.resize(dccRanges.size()); + for (std::size_t index = 0; index < metadata.size(); ++index) dccCopies[index + 1] = comparedMetadata[index] ? comparedCopy(dccRanges[index + 1], std::move(landedBytes[index])) : DccCopy{}; return true; } @@ -572,11 +613,19 @@ private: dccRanges.push_back({use.address, static_cast(colorBytes), 0}); dccRanges.push_back({use.dccAddress, dccBytes, 0}); if (use.cmaskAddress != 0) dccRanges.push_back({use.cmaskAddress, use.cmaskBytes, 0}); - for (auto& range : dccRanges) { + dccCopies.assign(dccRanges.size(), {}); + std::vector watched; + for (std::size_t index = 0; index < dccRanges.size(); ++index) { + auto& range = dccRanges[index]; GuestMemory::CheckRange(reinterpret_cast(range.address), range.bytes, 256); + if (!WatchedAfterSettle(context, range.address, range.bytes) && ComparedHostImportCovers(context, range.address, range.bytes)) { + dccCopies[index].compared = true; + continue; + } Require(GuestMemory::Watched(range.address, range.bytes), "initial multisampled DCC contents require watched guest memory"); range.generation = collectBlocks(range.address, range.bytes); Require(range.generation != 0, "initial multisampled DCC contents have no write generation"); + watched.push_back(range); } for (std::size_t first = 0; first < dccRanges.size(); ++first) { for (std::size_t second = first + 1; second < dccRanges.size(); ++second) { @@ -591,17 +640,28 @@ private: fmaskRange = GuestMemory::UnchangedQuery{use.fmaskAddress, static_cast(DescribeSurface(resource).guestBytes), 0}; auto& range = *fmaskRange; GuestMemory::CheckRange(reinterpret_cast(range.address), range.bytes, 65536u); - Require(GuestMemory::Watched(range.address, range.bytes), "native FMASK contents require watched guest memory"); - range.generation = GuestMemory::CollectWritesUncached(range.address, range.bytes); - Require(range.generation != 0, "native FMASK contents have no write generation"); + fmaskCopy.compared = !WatchedAfterSettle(context, range.address, range.bytes) && ComparedHostImportCovers(context, range.address, range.bytes); + Require(fmaskCopy.compared || GuestMemory::Watched(range.address, range.bytes), "native FMASK contents require watched guest memory"); + if (!fmaskCopy.compared) { + range.generation = GuestMemory::CollectWritesUncached(range.address, range.bytes); + Require(range.generation != 0, "native FMASK contents have no write generation"); + } for (const auto& other : dccRanges) Require(range.address >= other.address + other.bytes || other.address >= range.address + range.bytes, "native FMASK overlaps its color or metadata planes"); } - std::vector dcc(dccRanges[1].bytes); - GuestMemory::Read(use.dccAddress, dcc); - std::vector cmask(use.cmaskBytes); - if (!cmask.empty()) GuestMemory::Read(use.cmaskAddress, cmask); - const auto clear = MultisampledDccClear(use, dcc, cmask); - Require(GuestMemory::UnchangedSinceAll(dccRanges) && (!fmaskRange || GuestMemory::UnchangedSinceAll(std::span(&*fmaskRange, 1u))), "initial multisampled DCC contents changed during import"); + std::array, 3> bytes{}; + for (std::size_t index = 0; index < dccRanges.size(); ++index) { + if (index == 0 && !compared(0)) continue; + bytes[index].resize(dccRanges[index].bytes); + GuestMemory::Read(dccRanges[index].address, bytes[index]); + } + std::vector fmaskBytes(fmaskCopy.compared ? fmaskRange->bytes : 0u); + if (fmaskCopy.compared) GuestMemory::Read(fmaskRange->address, fmaskBytes); + const auto clear = MultisampledDccClear(use, bytes[1], bytes[2]); + Require((watched.empty() || GuestMemory::UnchangedSinceAll(watched)) && (!fmaskRange || fmaskCopy.compared || GuestMemory::UnchangedSinceAll(std::span(&*fmaskRange, 1u))), "initial multisampled DCC contents changed during import"); + for (std::size_t index = 0; index < dccRanges.size(); ++index) { + if (compared(index)) dccCopies[index] = comparedCopy(dccRanges[index], std::move(bytes[index])); + } + if (fmaskCopy.compared) fmaskCopy = comparedCopy(*fmaskRange, std::move(fmaskBytes)); return clear; } @@ -621,19 +681,28 @@ private: Require(metadataValid->load(std::memory_order_acquire), "multisampled DCC metadata was released while its color target remains resident"); static_cast(MultisampledDccBytes(use)); Require(use.address == target.address && use.dccAddress == target.dccAddress && use.dccControl == target.dccControl && use.dccAlphaOnMsb == target.dccAlphaOnMsb && use.dccPipeAligned == target.dccPipeAligned && use.cmaskAddress == target.cmaskAddress && use.cmaskBytes == target.cmaskBytes && use.tileMode == target.tileMode && use.elementBytes == target.elementBytes && use.pipeBankXor == target.pipeBankXor, "multisampled DCC target layout or metadata changed"); - const auto ranges = std::span(dccRanges).first(metadataRefilled ? 1u : dccRanges.size()); - for (const auto& range : ranges) requireDccRange(GuestMemory::Watched(range.address, range.bytes), range, "multisampled DCC target reuse requires watched guest memory"); - for (const auto& range : ranges) { - GuestMemory::CollectWritesUncached(range.address, range.bytes); - requireDccRange(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation) || !GuestMemory::StoredOver(range.address, range.bytes, range.generation), range, "writes to a resident multisampled DCC target or its metadata are not implemented"); + const auto ranges = metadataRefilled ? std::size_t{1} : dccRanges.size(); + for (std::size_t index = 0; index < ranges; ++index) requireDccRange(compared(index) || GuestMemory::Watched(dccRanges[index].address, dccRanges[index].bytes), dccRanges[index], "multisampled DCC target reuse requires watched guest memory"); + for (std::size_t index = 0; index < ranges; ++index) { + const auto& range = dccRanges[index]; + if (compared(index)) { + requireDccRange(copyCurrent(range, dccCopies[index]), range, "writes to a resident multisampled DCC target or its metadata are not implemented"); + } else { + GuestMemory::CollectWritesUncached(range.address, range.bytes); + requireDccRange(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation) || !GuestMemory::StoredOver(range.address, range.bytes, range.generation), range, "writes to a resident multisampled DCC target or its metadata are not implemented"); + } if (auto* recorder = Recorder::Active(); recorder != nullptr) requireDccRange(!recorder->PendingWriteOverlaps(range.address, range.bytes), range, "pending writes to a resident multisampled DCC target or its metadata are not implemented"); } if (fmaskRange) { Require(use.fmaskAddress == target.fmaskAddress && use.fmaskPipeBankXor == target.fmaskPipeBankXor, "native FMASK target layout changed"); const auto& range = *fmaskRange; - requireDccRange(GuestMemory::Watched(range.address, range.bytes), range, "native FMASK sampling requires watched guest memory"); - GuestMemory::CollectWritesUncached(range.address, range.bytes); - requireDccRange(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation) || !GuestMemory::StoredOver(range.address, range.bytes, range.generation), range, "writes to a resident native FMASK are not implemented"); + requireDccRange(fmaskCopy.compared || WatchedAfterSettle(context, range.address, range.bytes), range, "native FMASK sampling requires watched guest memory"); + if (fmaskCopy.compared) { + requireDccRange(copyCurrent(range, fmaskCopy), range, "writes to a resident native FMASK are not implemented"); + } else { + GuestMemory::CollectWritesUncached(range.address, range.bytes); + requireDccRange(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation) || !GuestMemory::StoredOver(range.address, range.bytes, range.generation), range, "writes to a resident native FMASK are not implemented"); + } if (auto* recorder = Recorder::Active(); recorder != nullptr) requireDccRange(!recorder->PendingWriteOverlaps(range.address, range.bytes), range, "pending writes to a resident native FMASK are not implemented"); } } @@ -660,6 +729,7 @@ struct GuardedRange { std::uint32_t samples; std::shared_ptr> active; std::shared_ptr> metadataValid; + std::shared_ptr> hostOnly; bool fmask = false; }; @@ -734,7 +804,7 @@ void retireUnmapped(std::uintptr_t address, std::size_t bytes) { anyGuarded().store(!guarded().empty(), std::memory_order_release); } -void guard(const Context& context, const ColorTarget& target, const std::shared_ptr>& active, const std::shared_ptr>& metadataValid) { +void guard(const Context& context, const ColorTarget& target, const std::shared_ptr>& active, const std::shared_ptr>& metadataValid, const std::shared_ptr>& hostOnly) { auto bytes = std::max(target.bytes * target.samples, static_cast(target.extent.width) * target.extent.height * target.elementBytes * target.samples); if (MultisampledBlockLog2(target.tileMode) == 16u && target.tileMode != ColorTileMode::Standard64KB && std::has_single_bit(target.elementBytes) && std::has_single_bit(target.samples)) { const auto log2Samples = std::countr_zero(target.samples); @@ -746,11 +816,11 @@ void guard(const Context& context, const ColorTarget& target, const std::shared_ bytes = align(target.extent.width, blockWidth) * align(target.extent.height, blockHeight) * target.elementBytes * target.samples; } std::unique_lock lock(guardedMutex()); - guarded().push_back({context.device, target.address, bytes, target.samples, active}); + guarded().push_back({context.device, target.address, bytes, target.samples, active, nullptr, hostOnly}); if (target.dccAddress != 0) { - guarded().push_back({context.device, target.dccAddress, MultisampledDccBytes(target), target.samples, active, metadataValid}); - if (target.cmaskAddress != 0) guarded().push_back({context.device, target.cmaskAddress, target.cmaskBytes, target.samples, active, metadataValid}); - if (target.samples == 8u && target.fmaskAddress != 0u && target.cmaskAddress != 0u) guarded().push_back({context.device, target.fmaskAddress, DescribeSurface(fmaskResource(target)).guestBytes, target.samples, active, metadataValid}); + guarded().push_back({context.device, target.dccAddress, MultisampledDccBytes(target), target.samples, active, metadataValid, hostOnly}); + if (target.cmaskAddress != 0) guarded().push_back({context.device, target.cmaskAddress, target.cmaskBytes, target.samples, active, metadataValid, hostOnly}); + if (target.samples == 8u && target.fmaskAddress != 0u && target.cmaskAddress != 0u) guarded().push_back({context.device, target.fmaskAddress, DescribeSurface(fmaskResource(target)).guestBytes, target.samples, active, metadataValid, hostOnly}); } anyGuarded().store(true, std::memory_order_release); InstallResidentImageReadGuard_nid_no_patch(); @@ -770,6 +840,13 @@ void discardRetiredTargets() { std::erase_if(targets(), [](const auto& target) { return !target->active->load(std::memory_order_acquire); }); } +bool holdHostOnly(const MultisampleTarget& target) { + std::unique_lock lock(guardedMutex()); + if (!target.active->load(std::memory_order_acquire)) return false; + target.hostOnly->store(true, std::memory_order_release); + return true; +} + MultisampleTarget& acquire(const Context& context, const ColorTarget& requested) { discardRetiredTargets(); for (const auto& target : targets()) { @@ -780,14 +857,21 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& requested) if (recreated(target->target, use)) { { std::unique_lock lock(guardedMutex()); + if (!target->active->load(std::memory_order_acquire)) continue; std::erase_if(guarded(), [&](const auto& range) { return range.active == target->active; }); anyGuarded().store(!guarded().empty(), std::memory_order_release); } target->Recreate(use); - guard(context, use, target->active, target->metadataValid); + target->hostOnly->store(true, std::memory_order_release); + guard(context, use, target->active, target->metadataValid, target->hostOnly); target->ApplyFastClear(use); return *target; } + { + std::unique_lock lock(guardedMutex()); + if (!target->active->load(std::memory_order_acquire)) continue; + target->hostOnly->store(true, std::memory_order_release); + } if (target->fromTexture && use.cmaskAddress != 0) { target->fromTexture = false; target->cmask = MultisampledCmask(use.cmaskAddress, use.cmaskBytes); @@ -803,14 +887,14 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& requested) target->Keep(); targets().push_back(std::move(target)); auto& created = *targets().back(); - guard(context, use, created.active, created.metadataValid); + guard(context, use, created.active, created.metadataValid, created.hostOnly); if (use.cmaskFastClear && use.dccAddress == 0) created.cmask.SeedCode(CurrentUniformMetadataByte(use.cmaskAddress, static_cast(use.cmaskBytes))); created.ApplyFastClear(use); created.Keep(); return created; } -MultisampleTarget& createFilled(const Context& context, const GuestTextureResource& resource, VkFormat format, const char* access) { +MultisampleTarget& createFilled(const Context& context, const GuestTextureResource& resource, VkFormat format, const char* access, bool written) { const auto refuse = [&](const std::string& reason) { char text[192]; std::snprintf(text, sizeof(text), "AGC graphics: %s the %ux%u multisampled texture 0x%llx of %u samples, which no draw rendered to and ", access, resource.width, resource.height, static_cast(resource.baseAddress), resource.samples); @@ -877,7 +961,8 @@ MultisampleTarget& createFilled(const Context& context, const GuestTextureResour target->Keep(); targets().push_back(std::move(target)); auto& created = *targets().back(); - guard(context, use, created.active, created.metadataValid); + created.hostOnly->store(written, std::memory_order_release); + guard(context, use, created.active, created.metadataValid, created.hostOnly); created.colorFill.NoteFill(use.address, static_cast(bytes), value); created.fromTexture = true; created.htileAddress = resource.dccAddress; @@ -1177,7 +1262,7 @@ std::shared_ptr MultisampleTargetTexture(const Context& context, const } if (other == nullptr) { Require(Recorder::Active() != nullptr, "a multisampled texture is only sampled by recorded draws and dispatches"); - return createFilled(context, resource, format, "sampling").Sampled(components, viewType); + return createFilled(context, resource, format, "sampling", false).Sampled(components, viewType); } char text[320]; std::snprintf(text, sizeof(text), "AGC graphics: sampling multisampled color target 0x%llx (%ux%u, %u samples, vk format %d) as a %ux%u texture of %u samples and guest format %u (vk %d) is not implemented", static_cast(resource.baseAddress), other->target.extent.width, other->target.extent.height, other->target.samples, static_cast(other->target.format), resource.width, resource.height, resource.samples, resource.format, static_cast(format)); @@ -1200,7 +1285,7 @@ std::shared_ptr MultisampleTargetFirstSample(const Context& context, co } if (other == nullptr) { Require(Recorder::Active() != nullptr, "a multisampled texture is only sampled by recorded draws and dispatches"); - return createFilled(context, resource, format, "sampling").FirstSample(components); + return createFilled(context, resource, format, "sampling", false).FirstSample(components); } char text[320]; std::snprintf(text, sizeof(text), "AGC graphics: reading sample 0 of multisampled color target 0x%llx (%ux%u, %u samples, vk format %d) as a %ux%u texture of %u samples and guest format %u (vk %d) is not implemented", static_cast(resource.baseAddress), other->target.extent.width, other->target.extent.height, other->target.samples, static_cast(other->target.format), resource.width, resource.height, resource.samples, resource.format, static_cast(format)); @@ -1221,12 +1306,15 @@ VkImageView MultisampleTargetStorageView(const Context& context, const GuestText if (!target->active->load(std::memory_order_acquire)) continue; if (target->context.device != context.device || target->target.surfaceAddress != resource.baseAddress) continue; const auto& rendered = target->target; - if (sameStorageForm(context, rendered.format, format) && rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0) return target->Storage(StorageFormatForGuest(context, resource.format)); + if (sameStorageForm(context, rendered.format, format) && rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0) { + if (!holdHostOnly(*target)) continue; + return target->Storage(StorageFormatForGuest(context, resource.format)); + } other = target.get(); } if (other == nullptr) { Require(Recorder::Active() != nullptr, "a multisampled color target is only written by recorded dispatches"); - return createFilled(context, resource, format, "writing").Storage(StorageFormatForGuest(context, resource.format)); + return createFilled(context, resource, format, "writing", true).Storage(StorageFormatForGuest(context, resource.format)); } char text[320]; std::snprintf(text, sizeof(text), "AGC graphics: storage image access to multisampled color target 0x%llx (%ux%u, %u samples, vk format %d) as a %ux%u texture of %u samples and guest format %u (vk %d) is not implemented", static_cast(resource.baseAddress), other->target.extent.width, other->target.extent.height, other->target.samples, static_cast(other->target.format), resource.width, resource.height, resource.samples, resource.format, static_cast(format)); @@ -1266,10 +1354,13 @@ void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, bool MultisampledColorFillAbsorbs(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) { if (bytes == 0) return false; std::lock_guard lock(targetsMutex()); - return std::any_of(targets().begin(), targets().end(), [&](const auto& target) { + bool absorbed = false; + for (const auto& target : targets()) { VkClearColorValue clear{}; - return target->active->load(std::memory_order_acquire) && target->target.address == address && MultisampledColorBytes(target->target) == bytes && MultisampledFillClearColor(target->target, pattern, clear); - }); + if (!target->active->load(std::memory_order_acquire) || target->target.address != address || MultisampledColorBytes(target->target) != bytes || !MultisampledFillClearColor(target->target, pattern, clear)) continue; + absorbed = holdHostOnly(*target) || absorbed; + } + return absorbed; } void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) { @@ -1327,7 +1418,7 @@ void GuardOneFragmentFmask(const Context& context, const ColorTarget& target) { const auto bytes = std::max((bits / 8u + 0xffffu) & ~0xffffull, 0x10000u); std::unique_lock lock(guardedMutex()); const bool known = std::ranges::any_of(guarded(), [&](const auto& range) { return range.fmask && range.device == context.device && range.address == target.fmaskAddress && range.bytes == bytes; }); - if (!known) guarded().push_back({context.device, target.fmaskAddress, bytes, target.coverageSamples, std::make_shared>(true), nullptr, true}); + if (!known) guarded().push_back({context.device, target.fmaskAddress, bytes, target.coverageSamples, std::make_shared>(true), nullptr, std::make_shared>(true), true}); anyGuarded().store(true, std::memory_order_release); lock.unlock(); InstallResidentImageReadGuard_nid_no_patch(); @@ -1340,9 +1431,8 @@ void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const void RequireNoMultisampleColorTarget(std::uint64_t address, std::size_t bytes, const char* access) { if (bytes == 0 || !anyGuarded().load(std::memory_order_acquire)) return; - std::shared_lock lock(guardedMutex()); - for (const auto& range : guarded()) { - if (address >= range.address + range.bytes || range.address >= address + bytes) continue; + const auto overlaps = [&](const GuardedRange& range) { return address < range.address + range.bytes && range.address < address + bytes; }; + const auto refuse = [&](const GuardedRange& range) { char text[256]; if (range.fmask) { std::snprintf(text, sizeof(text), "AGC graphics: %s of 0x%llx+0x%zx overlaps the FMASK 0x%llx+0x%llx of a one-fragment color target of %u samples drawn on the host, which does not write it: guest access to it is not implemented", access, static_cast(address), bytes, static_cast(range.address), static_cast(range.bytes), range.samples); @@ -1350,7 +1440,27 @@ void RequireNoMultisampleColorTarget(std::uint64_t address, std::size_t bytes, c } std::snprintf(text, sizeof(text), "AGC graphics: %s of 0x%llx+0x%zx overlaps multisampled color target 0x%llx+0x%llx (%u samples), whose samples are only on the host: guest access to it is not implemented", access, static_cast(address), bytes, static_cast(range.address), static_cast(range.bytes), range.samples); throw std::runtime_error(text); + }; + { + std::shared_lock lock(guardedMutex()); + bool retirable = false; + for (const auto& range : guarded()) { + if (!overlaps(range)) continue; + if (range.hostOnly->load(std::memory_order_acquire)) refuse(range); + retirable = true; + } + if (!retirable) return; } + std::unique_lock lock(guardedMutex()); + std::vector>> retired; + for (const auto& range : guarded()) { + if (!overlaps(range)) continue; + if (range.hostOnly->load(std::memory_order_acquire)) refuse(range); + retired.push_back(range.active); + } + for (const auto& active : retired) active->store(false, std::memory_order_release); + std::erase_if(guarded(), [&](const auto& range) { return std::find(retired.begin(), retired.end(), range.active) != retired.end(); }); + anyGuarded().store(!guarded().empty(), std::memory_order_release); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index fcad4ff7bf..1dd1e6c450 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -321,7 +321,7 @@ void appendKey(std::vector& key, const TValue& value) { bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& state, const VertexInputLayout& input, const PipelineSetLayouts& resources, std::span shaders) { using Stage = ShaderRecompiler::ShaderStage; - if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || BarycentricPatches(state, shaders) || SplitsAlternateAlpha(state) || std::ranges::any_of(shaders, [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->functionLdsBuffer; }) || FragmentInterlock(shaders)) return false; + if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || (state.stages.tessellation && !context.tessellationPipelineCache) || state.depthBiasPerFace || state.rectList || BarycentricPatches(state, shaders) || SplitsAlternateAlpha(state) || std::ranges::any_of(shaders, [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->functionLdsBuffer; }) || FragmentInterlock(shaders)) return false; keys = {}; for (const auto& shader : shaders) { const auto identity = shader.program->Identity(); @@ -723,20 +723,21 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput prebuilt = true; return; } + const VkPipelineCache pipelineCache = state.stages.tessellation && !context.tessellationPipelineCache ? VK_NULL_HANDLE : context.pipelineCache; if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, false, _modules); else { auto pipelineCacheLock = context.LockPipelineCacheForUse(); - Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines"); + Check(context.Function("vkCreateGraphicsPipelines")(context.device, pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines"); } if (alternateAlpha) { blend.pAttachments = secondChannels.data(); auto pipelineCacheLock = context.LockPipelineCacheForUse(); - Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &secondChannel), "vkCreateGraphicsPipelines"); + Check(context.Function("vkCreateGraphicsPipelines")(context.device, pipelineCache, 1, &pipelineInfo, nullptr, &secondChannel), "vkCreateGraphicsPipelines"); } if (state.depthBiasPerFace) { raster.cullMode = VK_CULL_MODE_FRONT_BIT; auto pipelineCacheLock = context.LockPipelineCacheForUse(); - Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &backFaces), "vkCreateGraphicsPipelines"); + Check(context.Function("vkCreateGraphicsPipelines")(context.device, pipelineCache, 1, &pipelineInfo, nullptr, &backFaces), "vkCreateGraphicsPipelines"); } for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); _modules.clear(); @@ -1126,6 +1127,14 @@ struct PipelineStore { std::uint64_t evicted = 0; PipelineStoreCounters totals; std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now(); + std::size_t capacity = 0; + bool fixedCapacity = false; + std::list evictedOrder; + std::unordered_map::iterator> evictedKeys; + std::size_t evictionsSinceCheck = 0; + std::size_t returnsSinceCheck = 0; + static constexpr std::size_t BaseEntries = 1024; + static constexpr std::size_t MaximumEntries = 4096; }; // Never destroyed: the pipelines belong to a device that may already be gone when statics die, and @@ -1154,9 +1163,41 @@ std::list::iterator abandon(PipelineStore& store, std::lis return store.entries.erase(it); } -std::size_t storeBound() { - static const auto bound = static_cast(std::max(1u, PipelineSwitch("APS5_PIPELINE_STORE_ENTRIES", 1024u))); - return bound; +std::size_t storeBound(PipelineStore& store) { + if (store.capacity == 0) { + const auto set = PipelineSwitch("APS5_PIPELINE_STORE_ENTRIES", 0u); + store.fixedCapacity = set != 0; + store.capacity = store.fixedCapacity ? static_cast(set) : PipelineStore::BaseEntries; + } + return store.capacity; +} + +void noteStoreEviction(PipelineStore& store, std::uint64_t hash) { + if (store.fixedCapacity) return; + if (const auto found = store.evictedKeys.find(hash); found != store.evictedKeys.end()) store.evictedOrder.erase(found->second); + store.evictedOrder.push_front(hash); + store.evictedKeys[hash] = store.evictedOrder.begin(); + while (store.evictedOrder.size() > store.capacity) { + store.evictedKeys.erase(store.evictedOrder.back()); + store.evictedOrder.pop_back(); + } + if (++store.evictionsSinceCheck < store.capacity / 32u) return; + const bool churning = store.returnsSinceCheck * 2u > store.evictionsSinceCheck; + store.evictionsSinceCheck = 0; + store.returnsSinceCheck = 0; + if (!churning || store.capacity >= PipelineStore::MaximumEntries) return; + const auto previous = store.capacity; + store.capacity = std::min(store.capacity * 2u, PipelineStore::MaximumEntries); + std::fprintf(stderr, "[pipecache] bound %zu -> %zu pipelines: pipelines it evicted keep coming back\n", previous, store.capacity); +} + +void noteStoreReturn(PipelineStore& store, std::uint64_t hash) { + if (store.fixedCapacity) return; + const auto found = store.evictedKeys.find(hash); + if (found == store.evictedKeys.end()) return; + store.evictedOrder.erase(found->second); + store.evictedKeys.erase(found); + ++store.returnsSinceCheck; } void reportPipelines(PipelineStore& store) { @@ -1228,12 +1269,12 @@ std::shared_ptr CachedPipeline(const Context& context, const State& st } ++store.misses; ++store.totals.misses; + noteStoreReturn(store, hash); } // Built outside the mutex: a draw that finds its pipeline resident is not held behind another // thread's build (the journal replay builds here too), and a device's driver compiles in // parallel when several threads build at once. auto pipeline = std::make_shared(context, state, vertexInput, resources, shaders, attachmentLayout); - const auto bound = storeBound(); const bool replaying = PipelineJournalReplaying(); { std::lock_guard lock(store.mutex); @@ -1251,7 +1292,10 @@ std::shared_ptr CachedPipeline(const Context& context, const State& st } // A replayed pipeline enters at the least recently used end and never displaces a draw's: // the title's own pipelines evict it first, and a full store takes none. - if (replaying && store.entries.size() >= bound) return pipeline; + if (replaying && store.entries.size() >= storeBound(store)) { + if (store.fixedCapacity || store.capacity >= PipelineStore::MaximumEntries) return pipeline; + store.capacity = std::min(store.capacity * 2u, PipelineStore::MaximumEntries); + } if (replaying) { store.entries.push_front({context.device, context.bufferPool, context.bufferPool != nullptr, hash, std::vector(key), pipeline}); store.index[hash] = store.entries.begin(); @@ -1259,12 +1303,14 @@ std::shared_ptr CachedPipeline(const Context& context, const State& st store.entries.push_back({context.device, context.bufferPool, context.bufferPool != nullptr, hash, std::vector(key), pipeline}); store.index[hash] = std::prev(store.entries.end()); } - while (store.entries.size() > bound) { + while (store.entries.size() > storeBound(store)) { // Only an entry no recorded draw still holds may go (Kept keeps its shared_ptr until the fence). const auto victim = std::find_if(store.entries.begin(), store.entries.end(), [](const PipelineStore::Entry& entry) { return entry.pipeline.use_count() == 1; }); if (victim == store.entries.end()) break; + const auto evictedHash = victim->hash; store.index.erase(victim->hash); store.entries.erase(victim); + noteStoreEviction(store, evictedHash); ++store.evicted; ++store.totals.evicted; } @@ -1283,10 +1329,16 @@ PipelineStoreCounters PipelineStoreTotals() { std::lock_guard lock(store.mutex); auto totals = store.totals; totals.cached = store.entries.size(); - totals.bound = storeBound(); + totals.bound = storeBound(store); return totals; } +bool PipelineStoreCanGrow() { + auto& store = Pipelines(); + std::lock_guard lock(store.mutex); + return storeBound(store) < PipelineStore::MaximumEntries && !store.fixedCapacity; +} + void ClearCachedPipelines(VkDevice device) { auto& store = Pipelines(); std::lock_guard lock(store.mutex); @@ -1298,6 +1350,13 @@ void ClearCachedPipelines(VkDevice device) { store.index.erase(it->hash); it = store.entries.erase(it); } + if (store.entries.empty() && !store.fixedCapacity) { + store.capacity = 0; + store.evictedOrder.clear(); + store.evictedKeys.clear(); + store.evictionsSinceCheck = 0; + store.returnsSinceCheck = 0; + } ClearPipelineLibraries(device); ReleasePixelFunctionLds(device); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 7eba2c79ac..1745c0ed96 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -38,7 +38,7 @@ static_assert(sizeof(VkStencilOpState) == 28, "VkStencilOpState changed: update static_assert(sizeof(VkVertexInputBindingDescription) == 12, "VkVertexInputBindingDescription changed: update the recipe codec"); static_assert(sizeof(VkVertexInputAttributeDescription) == 16, "VkVertexInputAttributeDescription changed: update the recipe codec"); #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(State) == 832, "State changed: update CanonicalPipelineState and the recipe codec"); +static_assert(sizeof(State) == 840, "State changed: update CanonicalPipelineState and the recipe codec"); static_assert(sizeof(ShaderStages) == 112, "ShaderStages changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 40, "MeshConfiguration changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "TessellationConfiguration changed: update the recipe codec"); @@ -1346,10 +1346,13 @@ public: private: void run() { +#ifdef _WIN32 + if (!SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL)) std::fprintf(stderr, "[pipeline-journal] replay thread priority not lowered (error %lu)\n", GetLastError()); +#endif std::call_once(loadOnce, [&] { try { auto journal = Journal::Instance().Load(); - const auto limit = configured("APS5_PIPELINE_JOURNAL_ENTRIES", 1024u); + const auto limit = configured("APS5_PIPELINE_JOURNAL_ENTRIES", 4096u); if (journal.entries.size() > limit) journal.entries.erase(journal.entries.begin(), journal.entries.end() - static_cast(limit)); loaded = std::move(journal); } catch (const std::exception& error) { @@ -1435,7 +1438,7 @@ private: return; } const auto store = PipelineStoreTotals(); - if (store.cached >= store.bound) { + if (store.cached >= store.bound && !PipelineStoreCanGrow()) { counters.skipped.fetch_add(1, std::memory_order_relaxed); return; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index 77119254d5..0ccd993ac2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -185,7 +185,8 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip std::vector libraries; for (std::size_t part = 0; part < flags.size(); ++part) { if (part == 0 && mesh) continue; - const auto key = Combined(*partKeys[part], *contextKeys[part]); + auto key = Combined(*partKeys[part], *contextKeys[part]); + key.push_back(static_cast(optimize)); auto& cache = device.parts[part]; if (const auto found = cache.find(key); found != cache.end()) { libraries.push_back(found->second); @@ -195,7 +196,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip VkGraphicsPipelineLibraryCreateInfoEXT library{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_LIBRARY_CREATE_INFO_EXT, flags[part] == VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_OUTPUT_INTERFACE_BIT_EXT ? &rendering : &shaderRendering}; library.flags = flags[part]; VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &library}; - create.flags = VK_PIPELINE_CREATE_LIBRARY_BIT_KHR | VK_PIPELINE_CREATE_RETAIN_LINK_TIME_OPTIMIZATION_INFO_BIT_EXT; + create.flags = optimize ? VK_PIPELINE_CREATE_LIBRARY_BIT_KHR | VK_PIPELINE_CREATE_RETAIN_LINK_TIME_OPTIMIZATION_INFO_BIT_EXT : VK_PIPELINE_CREATE_LIBRARY_BIT_KHR; create.pDynamicState = info.pDynamicState; create.layout = partLayouts[part]; switch (flags[part]) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 80eb8c355d..3abc0aec26 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -8,6 +8,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" #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/SampleCounter_spv.h" #include "prx/libSceAgcDriver/Graphics/shaders/SampleDumps_spv.h" @@ -3134,6 +3135,26 @@ std::uint64_t Recorder::PendingWriterSerial(std::uint64_t address, std::size_t b return 0; } +std::uint64_t Recorder::PendingWriterSerial(std::uint64_t address, std::size_t bytes, std::span exceptNotes) const { + if (exceptNotes.empty()) return PendingWriterSerial(address, bytes); + if (bytes == 0) return 0; + const auto end = address + bytes; + const auto writes = [&](const Batch& batch) { + for (std::size_t index = 0; index < batch.writes.size(); ++index) { + const auto& [begin, finish] = batch.writes[index]; + if (address >= finish || begin >= end) continue; + if (index < batch.writeNotes.size() && std::find(exceptNotes.begin(), exceptNotes.end(), batch.writeNotes[index]) != exceptNotes.end()) continue; + return true; + } + return readOverlap(batch, address, end, ReadKind::AddressBased).has_value(); + }; + if (open != nullptr && writes(*open)) return OpenSerial(); + for (auto it = inFlight.rbegin(); it != inFlight.rend(); ++it) { + if (writes(**it)) return (*it)->serial; + } + return 0; +} + bool Recorder::signaled(const Batch& batch) const { return batch.submitted && fenceStatus(batch.fence) == VK_SUCCESS; } @@ -3659,6 +3680,7 @@ void Recorder::Submit() { submission.pSignalSemaphores = &timeline; } if (hold != VK_NULL_HANDLE || timeline != VK_NULL_HANDLE) submission.pNext = &timelineInfo; + DestroyDeferredAtIdle(context); const auto submitStart = std::chrono::steady_clock::now(); const auto result = function(queueSubmit, "vkQueueSubmit")(context.queue, 1, &submission, batch->fence); if (result == VK_ERROR_DEVICE_LOST) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp index 4f30207def..ea37fed7ef 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DeferredDestruction.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -381,6 +382,7 @@ void CommandBatch::Submit() { submission.commandBufferCount = 1; submission.pCommandBuffers = &commands; timing.Mark("command_end"); + DestroyDeferredAtIdle(context); Check(context.Function("vkQueueSubmit")(context.queue, 1, &submission, fence), "vkQueueSubmit graphics"); pending = true; submitted = true; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 84bc841971..1c06f2639a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -66,7 +66,7 @@ VkComponentSwizzle ComponentSwizzleFor(std::uint8_t dstSel) { } VkComponentMapping ViewComponents(const GuestTextureResource& resource) { - if (IsConvertedTextureFormat(resource.format) && !IsRepackedTextureFormat(resource.format)) return {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A}; + if (IsConvertedTextureFormat(resource.format) && !IsRepackedTextureFormat(resource.format) && resource.format != static_cast(ShaderRecompiler::IrBufferFormat::Format11_11_10Float)) return {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A}; const auto channel = [&](std::uint8_t dstSel) { return TextureComponentChannel(resource.format, ComponentSwizzleFor(dstSel)); }; return {channel(resource.dstSelX), channel(resource.dstSelY), channel(resource.dstSelZ), channel(resource.dstSelW)}; } @@ -624,10 +624,7 @@ std::uint64_t storageBudget(const Context& context, std::uint64_t required = 0) cached.device = context.physical; cached.read = now; cached.budget = StorageTextureBudget(context.memory, &reported, SampledTextureMemory(), storageCacheBytes()); - cached.ceiling = cached.budget; - if (const auto heap = largestDeviceLocalHeap(context.memory); heap && reported.heapBudget[*heap] != 0) { - cached.ceiling = std::max(512ull << 20u, std::min(TextureCacheBudget(context.memory), reported.heapBudget[*heap] / 4u)); - } + cached.ceiling = StorageTextureCeiling(context.memory, &reported); }; if (cached.device != context.physical || now - cached.read >= std::chrono::seconds(1)) read(); const auto needed = std::min(required, cached.ceiling); @@ -752,7 +749,14 @@ 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)); @@ -799,7 +803,7 @@ std::shared_ptr cachedTexture(const Context& context, std::spanFilledKeys()) { std::array probe{}; - if (!FillDccClear(ResolveTextureFormat(resource.format), *keys, resource.dccAlphaOnMsb, probe)) { + if (!FillDccClear(DccClearTextureFormat(resource.format), *keys, resource.dccAlphaOnMsb, probe)) { char text[256]; std::snprintf(text, sizeof(text), "AGC graphics: sampled texture 0x%llx (%ux%u format %u, dcc 0x%llx) reads %s DCC keys filled over its pending image, a clear value the format has no encoding for", static_cast(resource.baseAddress), resource.width, resource.height, resource.format, static_cast(resource.dccAddress), DccKeysName(*keys)); throw std::runtime_error(text); @@ -1185,9 +1189,9 @@ std::shared_ptr cachedStorageTexture(const Context& context, std const auto viewed = ArrayLayerWindow(requested, context.limits.maxImageArrayLayers); if (viewed.baseAddress != requested.baseAddress) guestBytes = 0; Require(viewed.dimension != TextureDimension::k2DMsaa, "storage image access to a multisampled texture is not implemented"); - RequireNoMultisampleTarget(viewed.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a storage image access"); - if (auto depth = DepthSurfaceStorage(context, words, viewed, mip)) return depth; const auto viewedBytes = guestBytes != 0 ? guestBytes : DescribeSurface(viewed).guestBytes; + RequireNoMultisampleTarget(viewed.baseAddress, static_cast(viewedBytes), "a storage image access"); + if (auto depth = DepthSurfaceStorage(context, words, viewed, mip)) return depth; if (viewedBytes >= (1ull << 30u)) { static std::atomic large{0}; const auto count = large.fetch_add(1) + 1; @@ -1283,14 +1287,19 @@ std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory) return std::max(2048ull << 20u, heap ? memory.memoryHeaps[*heap].size / 4u : 0u); } +std::uint64_t StorageTextureCeiling(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported) { + const auto heap = largestDeviceLocalHeap(memory); + if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory); + return std::max(512ull << 20u, std::min(memory.memoryHeaps[*heap].size / 2u, reported->heapBudget[*heap] / 2u)); +} + std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t sampledBytes, std::uint64_t storageBytes) { const auto heap = largestDeviceLocalHeap(memory); if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory); const auto budget = reported->heapBudget[*heap]; const auto usage = reported->heapUsage[*heap]; const auto reserved = usage - std::min(usage, sampledBytes + storageBytes) + sampledBytes + budget / 8u; - const auto cap = std::min(memory.memoryHeaps[*heap].size / 2u, budget / 2u); - return std::max(512ull << 20u, std::min(cap, budget > reserved ? budget - reserved : 0u)); + return std::min(StorageTextureCeiling(memory, reported), std::max(512ull << 20u, budget > reserved ? budget - reserved : 0u)); } std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes, std::uint64_t storageBytes) { @@ -2075,9 +2084,10 @@ void ShaderResources::buildPrepare(std::span shaders, cons const bool classified = element < binding.bufferWritten.size() && element < binding.bufferRead.size() && element < binding.bufferAtomic.size(); allocations[index].writeOnly = classified && allocations[index].guest && allocations[index].written && binding.bufferWritten[element] && !read && !atomic; const auto& push = shader.program->pushConstants; - if (!push.empty()) { + const auto pushDataBytes = shader.program->stencilReferences.empty() ? push.size() : shader.program->stencilReferenceDword * sizeof(std::uint32_t); + if (pushDataBytes != 0u) { const auto position = shader.program->memoryOffsetDword * 4u + element; - Require(position < push.size(), "guest buffer offset lies outside the shader's push constants"); + Require(position < pushDataBytes, "guest buffer offset lies outside the shader's push constants"); allocations[index].pushByte = static_cast(shader.pushConstantOffset + position); } else { allocations[index].dataByte = shader.program->memoryOffsetDword * 4u + element; @@ -2445,7 +2455,7 @@ void ShaderResources::buildComplete() { for (auto element = first; element < last; ++element) { const auto index = binding.imageAllocations[element]; if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLER) images.push_back({samplers[index]->Handle(), VK_NULL_HANDLE, VK_IMAGE_LAYOUT_UNDEFINED}); - else if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) images.push_back({VK_NULL_HANDLE, storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL}); + else if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) images.push_back({VK_NULL_HANDLE, storageAtomic64[index] ? storageTextures[index]->Atomic64View(storageMips[index], storageFirstLayer[index]) : storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL}); else images.push_back({VK_NULL_HANDLE, textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()}); } if (recorded) runs.emplace_back(write.dstArrayElement, write.descriptorCount); @@ -4384,7 +4394,8 @@ std::size_t ShaderResources::addGuestBuffer(std::span words Require(!written || !overlap(address, size, indexAddress, indexBytes), "writable shader buffer aliases the index buffer"); written = written || allWritten; const std::uint64_t recordBytes = descriptor.Stride() != 0 ? descriptor.Stride() : extent; - const bool swept = read && dispatchThreads != 0 && recordBytes != 0 && dispatchThreads * recordBytes * 2u >= byteSize; + constexpr std::uint64_t WidestBufferStoreBytes = 16; + const bool swept = (read || (context.discreteGpu && descriptor.Stride() > WidestBufferStoreBytes)) && dispatchThreads != 0 && recordBytes != 0 && dispatchThreads * recordBytes * 2u >= byteSize; if (written) guestMemory.AddWritable(address, size, atomic, swept); else { guestMemory.AddReadable(address, size); @@ -4492,12 +4503,11 @@ std::uint64_t ShaderResources::DataWordsHash(const CompiledShader& shader) { bool ShaderResources::DataWordsDiffer(const CompiledShader& shader) const { Require(shader.program != nullptr, "missing compiled shader"); const auto& program = *shader.program; - if (program.bindings.size() != bindings.size()) return true; - for (std::size_t index = 0; index < program.bindings.size(); ++index) { - const auto& binding = program.bindings[index]; + for (const auto& binding : program.bindings) { if (!DataRole(binding.role)) continue; - if (bindings[index].allocations.size() != 1) return true; - const auto& allocation = allocations[bindings[index].allocations.front()]; + const auto held = std::ranges::find_if(bindings, [&](const Binding& candidate) { return candidate.layout.binding == binding.binding; }); + if (held == bindings.end() || held->allocations.size() != 1) return true; + const auto& allocation = allocations[held->allocations.front()]; if (allocation.dataWords.size() != binding.guestDescriptor.size() || !std::equal(allocation.dataWords.begin(), allocation.dataWords.end(), binding.guestDescriptor.begin())) return true; } return false; @@ -4512,23 +4522,23 @@ bool ShaderResources::RefreshData(VkCommandBuffer commands, const CompiledShader bool ShaderResources::refreshData(VkCommandBuffer commands, const CompiledShader& shader, Recorder* recorder) { Require(shader.program != nullptr, "missing compiled shader"); const auto& program = *shader.program; - Require(program.bindings.size() == bindings.size(), "template bindings disagree with the shader"); bool recorded = false; // One [gputime] range of class TemplateDataRefresh around the updates, begun at the first one // (a refresh that finds every word equal records nothing and times nothing). auto timing = Recorder::NoTiming; std::uint64_t refreshedBytes = 0; - for (std::size_t index = 0; index < program.bindings.size(); ++index) { - const auto& binding = program.bindings[index]; + for (const auto& binding : program.bindings) { if (!DataRole(binding.role)) continue; - Require(bindings[index].allocations.size() == 1, "data binding without its buffer"); - auto& allocation = allocations[bindings[index].allocations.front()]; + const auto held = std::ranges::find_if(bindings, [&](const Binding& candidate) { return candidate.layout.binding == binding.binding; }); + Require(held != bindings.end(), "template bindings disagree with the shader"); + Require(held->allocations.size() == 1, "data binding without its buffer"); + auto& allocation = allocations[held->allocations.front()]; const auto size = binding.guestDescriptor.size() * sizeof(std::uint32_t); Require(allocation.buffer != nullptr && !allocation.guest && allocation.size == size && size <= MaxRefreshBytes, "template data buffer cannot take the dispatch's words"); if (allocation.dataWords.size() == binding.guestDescriptor.size() && std::equal(allocation.dataWords.begin(), allocation.dataWords.end(), binding.guestDescriptor.begin())) continue; if (binding.role == ShaderRecompiler::DescriptorRole::ShaderData) ValidateRuntimeResources(shader, binding.guestDescriptor); if (recorder != nullptr && timing == Recorder::NoTiming) timing = recorder->BeginGpuTiming(Recorder::CommandClass::TemplateDataRefresh); - writeDataWords(commands, bindings[index].allocations.front(), binding.guestDescriptor); + writeDataWords(commands, held->allocations.front(), binding.guestDescriptor); allocation.dataWords.assign(binding.guestDescriptor.begin(), binding.guestDescriptor.end()); refreshedBytes += size; recorded = true; @@ -5227,7 +5237,7 @@ void ShaderResources::resolveImageTables() { storageFirstLayer.push_back(firstLayer); storageWritten.push_back(true); storageAtomic.push_back(element < binding.imageAtomic.size() && binding.imageAtomic[element]); - storageAtomic64.push_back(false); + storageAtomic64.push_back(element < binding.imageAtomic64.size() && binding.imageAtomic64[element]); describedRanges.push_back({"storage", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress, true}); bindings[storageBinding].imageAllocations[absolute] = storageTextures.size() - 1u; continue; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index de94232f92..3c280c1312 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libc/include/General.hpp" #include @@ -314,13 +315,33 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint auto fixed = compare == 0 || compare == 7 ? 0u : face.compareMask; std::uint32_t invertMask = 0; std::uint32_t inverted = 0; + std::uint32_t logicMask = 0; + const auto logic = [](std::uint32_t op) { return op == 10 || op == 11 || op == 12 || op == 15; }; const auto convert = [&](std::uint32_t op, bool testFailed) { if (face.writeMask == 0) return VK_STENCIL_OP_KEEP; char detail[112]; - if (opValExport && ((op == 4 && !opValReference) || op == 5 || op == 6 || op == 8 || op == 9)) { + if (opValExport && ((op == 4 && !opValReference) || (op >= 5 && op != 7))) { std::snprintf(detail, sizeof(detail), "AGC graphics: stencil operation %u with a pixel shader that exports the stencil operation value is unsupported", op); throw std::runtime_error(detail); } + if (logic(op)) { + const auto bits = (op == 11 || op == 12 ? opValue : ~opValue) & face.writeMask; + if (bits == 0) return VK_STENCIL_OP_KEEP; + if (logicMask != 0 && logicMask != bits) { + std::snprintf(detail, sizeof(detail), "AGC graphics: stencil logic operations on write masks 0x%02x and 0x%02x are unsupported", logicMask, bits); + throw std::runtime_error(detail); + } + logicMask = bits; + if (op != 11) return op == 10 ? VK_STENCIL_OP_ZERO : VK_STENCIL_OP_INVERT; + if (testValExport) throw std::runtime_error("AGC graphics: stencil OR is unsupported with a pixel shader that exports the stencil test value"); + if ((~reference & fixed & bits) != 0) { + std::snprintf(detail, sizeof(detail), "AGC graphics: stencil OR of 0x%02x against test value 0x%02x (masks 0x%02x/0x%02x) is unsupported", opValue, test, face.compareMask, face.writeMask); + throw std::runtime_error(detail); + } + reference |= bits; + fixed |= bits; + return VK_STENCIL_OP_REPLACE; + } switch (op) { case 0: return VK_STENCIL_OP_KEEP; case 1: return VK_STENCIL_OP_ZERO; @@ -377,6 +398,15 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint if (inverted != active) throw std::runtime_error("AGC graphics: a stencil replacement drawn as an inversion beside another stencil operation is unsupported"); face.writeMask = invertMask; } + if (logicMask != 0) { + const auto other = [&](std::uint32_t op) { return op != 0 && !logic(op); }; + if ((other(ops & 0xfu) || other((ops >> 4u) & 0xfu) || other((ops >> 8u) & 0xfu)) && logicMask != face.writeMask) { + char detail[128]; + std::snprintf(detail, sizeof(detail), "AGC graphics: stencil operations 0x%03x combine a logic operation on write mask 0x%02x with write mask 0x%02x", ops & 0xfffu, logicMask, face.writeMask); + throw std::runtime_error(detail); + } + face.writeMask = logicMask; + } face.reference = reference; Require(!opValReference || compare == 0 || compare == 7 || face.compareMask == 0, "a stencil compare against the test value is unsupported with a pixel shader that exports the stencil operation value"); return face; @@ -953,12 +983,17 @@ struct DecodedColorFormat { 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"; return packing == ShaderRecompiler::ColorExportPacking::Float11_11_10 ? "11_11_10 float" : "10_11_11 unorm"; } +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; +} + // CB_COLOR_INFO FORMAT / NUMBER_TYPE / COMP_SWAP to a Vulkan attachment format. AMD formats list // components from the least significant bits, as Vulkan's non-packed formats do. -DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, std::uint32_t swap) { +DecodedColorFormat DecodeColorFormatRaw(std::uint32_t format, std::uint32_t number, std::uint32_t swap) { constexpr std::uint32_t unorm = 0, snorm = 1, sint = 5, srgb = 6, floating = 7; constexpr std::uint32_t uint = 4; const auto fail = [&]() -> DecodedColorFormat { @@ -1326,6 +1361,27 @@ ReversedBlend TranslateReversedBlend(std::uint32_t blendControl, std::uint8_t co return {blend, routing->code}; } +void ApplyDepthBoundsEmulation(const Context& context, State& state, const ShaderRecompiler::ShaderPixelStageInfo& pixel) { + if (!state.depthBoundsTest || !state.depth || context.depthBounds) return; + ValidateDepthBounds(context, state); + Require(!context.depthRangeUnrestricted, "the depth bounds test without the depthBounds feature is emulated only where stored depth stays in [0, 1]"); + Require(state.depth->format == VK_FORMAT_D32_SFLOAT || state.depth->format == VK_FORMAT_D32_SFLOAT_S8_UINT, "the depth bounds test on a normalized depth target needs the depthBounds feature"); + Require(!state.depthTest && !state.depthWrite, "the depth bounds test with depth testing needs the depthBounds feature"); + Require(!pixel.posZ && !pixel.depthExportEnable, "the depth bounds test from a pixel program that reads or exports depth needs the depthBounds feature"); + const auto keepsOnFail = [](const VkStencilOpState& face) { return (face.writeMask & 0xffu) == 0 || (face.failOp == VK_STENCIL_OP_KEEP && face.depthFailOp == VK_STENCIL_OP_KEEP); }; + Require(!state.stencilTest || (keepsOnFail(state.stencilFront) && keepsOnFail(state.stencilBack)), "the depth bounds test with a stencil test that writes on a failed test needs the depthBounds feature"); + const bool upper = state.minDepthBounds <= 0.0f; + Require(upper || state.maxDepthBounds >= 1.0f, "the depth bounds test with both bounds inside (0, 1) needs the depthBounds feature"); + const float bound = upper ? state.maxDepthBounds : state.minDepthBounds; + state.depthBoundsTest = false; + state.depthTest = true; + state.depthCompare = upper ? VK_COMPARE_OP_GREATER_OR_EQUAL : VK_COMPARE_OP_LESS_OR_EQUAL; + state.depthBias = false; + state.depthBiasPerFace = false; + state.viewport.minDepth = bound; + state.viewport.maxDepth = bound; +} + void ApplyDepthRemap(const Context& context, State& state) { const auto& viewport = state.viewport; const bool inRange = viewport.minDepth >= 0 && viewport.minDepth <= 1 && viewport.maxDepth >= 0 && viewport.maxDepth <= 1; @@ -1533,6 +1589,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { if (slotExport == 7 && ((read(cx, 0x31c + slot * 0xfu) >> 8u) & 7u) != 4u) throw std::runtime_error("AGC graphics: color export format 7 (UINT16_ABGR) into a target that is not unsigned integer is unsupported"); color.exportIndex = index; color.uintExport = slotExport == 7; + if (color.uintExport && color.packing == ShaderRecompiler::ColorExportPacking::Uint) color.packing = ShaderRecompiler::ColorExportPacking::None; if (color.samples == 1 && result.samples > 1 && result.colors.empty() && !result.depth && (read(cx, 0x292) & 1u) == 0) { const auto sampleInputs = read(cx, 0x1b3) & read(cx, 0x1b4) & (ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage)); Require(sampleInputs == 0 && (read(cx, 0x203) & 0x100u) == 0, "a one-sample color target under a multisampled rasterizer without MSAA_ENABLE is unsupported with a sample index or coverage input, or a sample-mask export"); @@ -1690,7 +1747,8 @@ 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::None) { + if (color.packing == ShaderRecompiler::ColorExportPacking::Uint) 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; if (slotExport == 5 || slotExport == 6) throw std::runtime_error("AGC graphics: color export format " + std::to_string(slotExport) + " into " + packed + " target is unsupported"); @@ -1715,6 +1773,10 @@ 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 (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; }; Require(addition(state.colorBlendOp) && addition(state.alphaBlendOp), "dual-source blending with a MIN or MAX operation is unsupported"); @@ -1912,11 +1974,17 @@ GuestTextureResource SurfaceForTarget(const ColorTarget& color) { surface.dstSelY = 5; surface.dstSelZ = 6; surface.dstSelW = 7; - surface.dccAddress = color.dccAddress; + surface.dccAddress = color.dccAddress != 0 ? color.dccAddress : color.chainDccAddress; surface.dccAlphaOnMsb = color.dccAlphaOnMsb; surface.dccPipeAligned = color.dccPipeAligned; return surface; } +DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, std::uint32_t swap) { + auto decoded = DecodeColorFormatRaw(format, number, swap); + if (number == 4u && decoded.packing == ShaderRecompiler::ColorExportPacking::None) decoded.packing = ShaderRecompiler::ColorExportPacking::Uint; + return decoded; +} + ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allowMultisampleDcc) { const auto stride = slot * 0xfu; @@ -2025,7 +2093,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo color.format = decoded.format; color.componentMapping = decoded.componentMapping; color.packing = decoded.packing; - if (color.packing != ShaderRecompiler::ColorExportPacking::None && (info & 0x10000000u) != 0) throw std::runtime_error("AGC graphics: DCC-compressed " + std::string(PackedColorTargetName(color.packing)) + " color targets are unsupported"); + if (WordPacked(color.packing) && (info & 0x10000000u) != 0) throw std::runtime_error("AGC graphics: DCC-compressed " + std::string(PackedColorTargetName(color.packing)) + " color targets are unsupported"); Require(color.packing != ShaderRecompiler::ColorExportPacking::Unorm10_11_11 || (info & 0x00040000u) == 0, "truncating (ROUND_MODE) 10_11_11 unorm color targets are unsupported"); Require(color.packing != ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 || (info & 0x00040000u) == 0, "truncating (ROUND_MODE) 4_4_4_4 unorm color targets are unsupported"); Require(color.packing != ShaderRecompiler::ColorExportPacking::Float11_11_10 || (info & 0x00040000u) != 0, "rounding (ROUND_MODE 0) 11_11_10 float color targets are unsupported"); @@ -2081,9 +2149,10 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo Require(color.fmaskAddress != 0, "a compressed one-fragment color target without an FMASK address is unsupported"); } if ((info & 0x10000000u) != 0) { + const auto dccHigh = find(cx, 0x3a8 + slot); + const auto dccBase = ((dccHigh == cx.end() ? 0ull : static_cast(dccHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x325 + stride)) << 8u); if (maxMip == 0) { - const auto dccHigh = find(cx, 0x3a8 + slot); - color.dccAddress = ((dccHigh == cx.end() ? 0ull : static_cast(dccHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x325 + stride)) << 8u); + color.dccAddress = dccBase; color.dccAlphaOnMsb = DccAlphaOnMsb(color.format, swap); color.dccPipeAligned = ((attrib3 >> 30u) & 1u) != 0; if (color.samples > 1) { @@ -2091,11 +2160,15 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo color.dccControl = read(cx, 0x31e + stride); if (!allowMultisampleDcc) Require((color.dccControl & 0x3ff80u) == 0, "multisampled DCC color transforms or lossy modes are not implemented"); } + } else if (!volume && color.arrayLastSlice == 0 && slice == 0) { + color.chainDccAddress = dccBase; + color.dccAlphaOnMsb = DccAlphaOnMsb(color.format, swap); + color.dccPipeAligned = ((attrib3 >> 30u) & 1u) != 0; } else { static bool reported = false; if (!reported) { reported = true; - std::fprintf(stderr, "[gpu] DCC keys of mipmapped color targets are ignored\n"); + std::fprintf(stderr, "[gpu] DCC keys of mipmapped array and 3D color targets are ignored\n"); } } } @@ -2192,7 +2265,7 @@ std::optional DecodeColorMetadataPass(const QueueState& queue Require(destination.samples == 1, "CB resolve into a multisampled target is unsupported"); Require(destination.cmaskAddress == 0, "CB resolve into a fast-clear color target is unsupported (the resolve does not update its CMASK)"); Require(source.format == destination.format && source.extent.width == destination.extent.width && source.extent.height == destination.extent.height, "CB resolve between different formats or extents is unsupported"); - if (source.packing != ShaderRecompiler::ColorExportPacking::None) throw std::runtime_error("AGC graphics: CB resolves of " + std::string(PackedColorTargetName(source.packing)) + " color targets are unsupported"); + if (WordPacked(source.packing)) throw std::runtime_error("AGC graphics: CB resolves of " + std::string(PackedColorTargetName(source.packing)) + " color targets are unsupported"); requireCovered(destination); pass.source = source; pass.targets.push_back(destination); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index d8394b6c5e..ada06eab1e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -123,6 +123,7 @@ struct StorageTraffic { std::map> writeBacks; std::array, 4> uploads{}; std::map> uploadReasons; + std::map> drawnStores; std::uint64_t directWriteBacks = 0; std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now(); }; @@ -132,6 +133,14 @@ StorageTraffic& Traffic() { return traffic; } +struct DrawnPending { + std::size_t images = 0; + std::uint64_t bytes = 0; + double oldestMs = 0; +}; + +DrawnPending drawnPending(); + void reportStorageTraffic(StorageTraffic& traffic) { const auto now = std::chrono::steady_clock::now(); if (now - traffic.lastReport < std::chrono::seconds(10)) return; @@ -154,12 +163,19 @@ void reportStorageTraffic(StorageTraffic& traffic) { std::snprintf(text, sizeof(text), " %llu/%.1f", static_cast(totals.first), totals.second / 1048576.0); line += " " + reason + text; } + std::string drawnLine; + for (const auto& [reason, totals] : traffic.drawnStores) { + std::snprintf(text, sizeof(text), " %llu/%.1f", static_cast(totals.first), totals.second / 1048576.0); + drawnLine += " " + reason + text; + } + const auto drawn = drawnPending(); 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\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)); + 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); traffic.writeBacks.clear(); traffic.uploadReasons.clear(); + traffic.drawnStores.clear(); traffic.directWriteBacks = 0; } @@ -175,7 +191,7 @@ void countStorageUpload(std::size_t path, std::uint64_t bytes) { reportStorageTraffic(traffic); } -void countStorageWriteBack(std::uint64_t bytes, bool direct) { +void countStorageWriteBack(std::uint64_t bytes, bool direct, bool drawn = false) { if (!LookupOutcomes::Profiled()) return; auto& traffic = Traffic(); std::lock_guard lock(traffic.mutex); @@ -183,6 +199,11 @@ void countStorageWriteBack(std::uint64_t bytes, bool direct) { ++totals.first; totals.second += bytes; if (direct) ++traffic.directWriteBacks; + if (drawn) { + auto& stores = traffic.drawnStores[flushReason != nullptr ? flushReason : "other"]; + ++stores.first; + stores.second += bytes; + } reportStorageTraffic(traffic); } @@ -1538,6 +1559,13 @@ struct PendingWrites { // Images taken out of `textures` by a FlushPending still storing them (see adjacentPendingUnchanged). std::vector flushing; std::atomic flushingCount{0}; + struct Drawn { + StorageTexture* texture; + std::uint64_t bytes; + std::chrono::steady_clock::time_point since; + }; + std::vector drawn; + std::atomic drawnCount{0}; }; PendingWrites& Pending() { @@ -1545,6 +1573,20 @@ PendingWrites& Pending() { return pending; } +DrawnPending drawnPending() { + DrawnPending result; + auto& pending = Pending(); + if (pending.drawnCount.load(std::memory_order_acquire) == 0) return result; + const auto now = std::chrono::steady_clock::now(); + std::lock_guard lock(pending.mutex); + result.images = pending.drawn.size(); + for (const auto& entry : pending.drawn) { + result.bytes += entry.bytes; + result.oldestMs = std::max(result.oldestMs, std::chrono::duration(now - entry.since).count()); + } + return result; +} + // See StorageTexture::PendingSerial: bumped after every mutation of the registry above. std::atomic pendingSerial{0}; @@ -3804,7 +3846,13 @@ void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) } } if (anyUnsaved && !GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) captureGuestBytes(&unsaved); - for (std::uint32_t layer = first; layer < first + count; ++layer) layerPending[layer] = true; + for (std::uint32_t layer = first; layer < first + count; ++layer) { + if (!layerPending[layer] && !completionKept.empty()) { + const auto end = layerBegin(layer) + layerBytes(layer); + for (auto at = layerBegin(layer) & ~std::uint64_t{65535}; at < end; at += 65536) completionKept[static_cast(at / 65536 - descriptor.baseAddress / 65536)] = 0; + } + layerPending[layer] = true; + } // 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. @@ -3827,6 +3875,10 @@ void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) } auto& pending = Pending(); std::lock_guard lock(pending.mutex); + if (!drawingTarget) { + pendingFromOthers = true; + if (drawnListed) drawnWhole = true; + } // The active recorder installs a hook that also waits for recorded work; without one (tests), // pending storage results alone are flushed. if (Recorder::Active() == nullptr) { @@ -3851,6 +3903,7 @@ void StorageTexture::reconcilePending() { auto& pending = Pending(); std::lock_guard lock(pending.mutex); const bool any = anyLayerPending(); + if (!any) forgetDrawn(); if (any == dirty) return; dirty = any; if (any) pending.textures.push_back(this); @@ -3864,6 +3917,215 @@ bool StorageTexture::Dirty() const { return dirty; } +constexpr std::size_t MaxDrawnRects = 16; + +bool StorageTexture::MarkDrawn(std::uint32_t slice, std::uint32_t count, VkRect2D scissor) { + struct Drawing { + bool& flag; + bool previous; + ~Drawing() { flag = previous; } + } drawing{drawingTarget, std::exchange(drawingTarget, true)}; + MarkSliceDirty(slice, count); + const auto clip = [](std::int64_t value, std::uint32_t limit) { return static_cast(std::clamp(value, 0, limit)); }; + const auto left = clip(scissor.offset.x, descriptor.width); + const auto top = clip(scissor.offset.y, descriptor.height); + const auto right = clip(std::int64_t{scissor.offset.x} + scissor.extent.width, descriptor.width); + const auto bottom = clip(std::int64_t{scissor.offset.y} + scissor.extent.height, descriptor.height); + const bool rectangle = proxyImage == VK_NULL_HANDLE && descriptor.tileMode == TextureTileMode::kLinear && !blockUnits && trackedLayers == 1 && descriptor.mipCount == 1 && arrayLayers == 1 && geometry.imageLayers == 1 && geometry.imageDepth == 1 && !geometry.thick && descriptor.dccAddress == 0; + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + if (!anyLayerPending()) return true; + if (!drawnListed) { + drawnListed = true; + drawnWhole = false; + drawnRects.clear(); + pending.drawn.push_back({this, guestBytes, std::chrono::steady_clock::now()}); + pending.drawnCount.store(pending.drawn.size(), std::memory_order_release); + } + if (!rectangle || pendingFromOthers) drawnWhole = true; + if (!drawnWhole && right > left && bottom > top) addDrawnRect({left, top, right, bottom}); + return !drawnWhole && drawnRects.size() <= MaxDrawnRects; +} + +void StorageTexture::addDrawnRect(DrawnRect rect) { + const auto inside = [](const DrawnRect& outer, const DrawnRect& inner) { return outer.left <= inner.left && outer.top <= inner.top && inner.right <= outer.right && inner.bottom <= outer.bottom; }; + if (std::any_of(drawnRects.begin(), drawnRects.end(), [&](const DrawnRect& kept) { return inside(kept, rect); })) return; + for (auto kept = drawnRects.begin(); kept != drawnRects.end();) { + const bool columns = kept->left == rect.left && kept->right == rect.right && kept->top <= rect.bottom && rect.top <= kept->bottom; + const bool rows = kept->top == rect.top && kept->bottom == rect.bottom && kept->left <= rect.right && rect.left <= kept->right; + if (!columns && !rows && !inside(rect, *kept)) { + ++kept; + continue; + } + rect = {std::min(kept->left, rect.left), std::min(kept->top, rect.top), std::max(kept->right, rect.right), std::max(kept->bottom, rect.bottom)}; + drawnRects.erase(kept); + kept = drawnRects.begin(); + } + drawnRects.push_back(rect); +} + +void StorageTexture::forgetDrawn() { + pendingFromOthers = false; + if (!drawnListed) return; + drawnListed = false; + drawnWhole = false; + drawnRects.clear(); + auto& pending = Pending(); + std::erase_if(pending.drawn, [this](const PendingWrites::Drawn& entry) { return entry.texture == this; }); + pending.drawnCount.store(pending.drawn.size(), std::memory_order_release); +} + +void StorageTexture::noteStore(std::uint64_t note) { + if (note == 0) return; + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + storeNotes.push_back(note); + if (storeNotes.size() > 8) storeNotes.erase(storeNotes.begin()); +} + +StorageTexture::DrawnArea StorageTexture::drawnArea() const { + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + if (!drawnListed) return {}; + DrawnArea area{true, !drawnWhole && !pendingFromOthers}; + if (!area.rect || drawnRects.empty()) return area; + area.left = area.top = std::numeric_limits::max(); + for (const auto& rect : drawnRects) { + area.left = std::min(area.left, rect.left); + area.top = std::min(area.top, rect.top); + area.right = std::max(area.right, rect.right); + area.bottom = std::max(area.bottom, rect.bottom); + } + area.rects = drawnRects; + return area; +} + +std::vector> StorageTexture::drawnSpans(const DrawnArea& area, std::span> keep) const { + std::vector> spans; + if (area.rects.empty()) return spans; + const auto& mip = mips.front(); + const auto elementBytes = static_cast(BytesPerElement(descriptor.format)); + const auto rowBase = descriptor.baseAddress + mip.tiledOffset; + std::vector> columns; + std::size_t next = 0; + for (auto row = area.top; row < area.bottom; ++row) { + columns.clear(); + for (const auto& rect : area.rects) { + if (rect.top <= row && row < rect.bottom) columns.emplace_back(rect.left, rect.right); + } + std::sort(columns.begin(), columns.end()); + const auto rowStart = rowBase + std::uint64_t{row} * mip.pitchBytes; + for (std::size_t first = 0; first < columns.size();) { + auto right = columns[first].second; + auto last = first + 1; + for (; last < columns.size() && columns[last].first <= right; ++last) right = std::max(right, columns[last].second); + const auto rowBegin = rowStart + columns[first].first * elementBytes; + const auto rowEnd = rowStart + right * elementBytes; + first = last; + while (next < keep.size() && keep[next].second <= rowBegin) ++next; + for (auto index = next; index < keep.size() && keep[index].first < rowEnd; ++index) { + const auto from = std::max(rowBegin, keep[index].first); + const auto to = std::min(rowEnd, keep[index].second); + if (from < to) spans.emplace_back(from, to); + } + } + } + return spans; +} + +std::uint64_t StorageTexture::storeDrawnRect(const DrawnArea& area, std::span segments, std::span> stores, const std::vector& saveUnits) { + const auto& mip = mips.front(); + const auto elementBytes = static_cast(BytesPerElement(descriptor.format)); + const auto rowBase = descriptor.baseAddress + mip.tiledOffset; + const auto width = area.right - area.left; + const auto height = area.bottom - area.top; + const auto rowBytes = std::uint64_t{width} * elementBytes; + const auto hullBegin = stores.front().first; + const auto hullBytes = static_cast(stores.back().second - hullBegin); + std::uint64_t storedBytes = 0; + for (const auto& [from, to] : stores) storedBytes += to - from; + auto scratch = std::make_shared(context, static_cast(rowBytes * height), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); + auto* recorder = Recorder::Active(); + std::unique_ptr batch; + VkCommandBuffer commands = VK_NULL_HANDLE; + auto timing = Recorder::NoTiming; + if (recorder != nullptr) { + recorder->FlushKeyStoresOverlapping(hullBegin, hullBytes); + recorder->FlushStoresOverlapping(hullBegin, hullBytes); + commands = recorder->Commands(); + timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageWriteBack); + recorder->Keep(scratch, scratch->Size()); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); + Recorder::CountBarriers(Recorder::CommandClass::StorageWriteBack, 3); + if (Recorder::BarrierValidate()) { + const std::pair read{image, false}; + recorder->NoteAccess(Recorder::CommandClass::StorageWriteBack, Recorder::Access{{}, stores, std::span(&read, 1), VK_PIPELINE_STAGE_TRANSFER_BIT}); + } + } else { + batch = std::make_unique(context); + commands = batch->Handle(); + } + VkImageMemoryBarrier toSource{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toSource.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + toSource.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + toSource.oldLayout = VK_IMAGE_LAYOUT_GENERAL; + toSource.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + toSource.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toSource.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toSource.image = image; + toSource.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, descriptor.mipCount, 0, geometry.imageLayers}; + const VkMemoryBarrier importReady{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT}; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &importReady, 0, nullptr, 1, &toSource); + VkBufferImageCopy region{}; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageOffset = {static_cast(area.left), static_cast(area.top), 0}; + region.imageExtent = {width, height, 1u}; + context.Function("vkCmdCopyImageToBuffer")(commands, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, scratch->Handle(), 1, ®ion); + const auto scratchRead = WholeBufferBarrier(scratch->Handle(), VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &scratchRead, 0, nullptr); + const auto scratchOffset = [&](std::uint64_t address) { + const auto offset = address - rowBase; + const auto row = offset / mip.pitchBytes; + return (row - area.top) * rowBytes + offset - row * mip.pitchBytes - area.left * elementBytes; + }; + const auto copyBuffer = context.Function("vkCmdCopyBuffer"); + for (const auto& segment : segments) { + std::vector copies; + for (const auto& [from, to] : stores) { + const auto begin = std::max(from, segment.begin); + const auto end = std::min(to, segment.end); + if (begin < end) copies.push_back({scratchOffset(begin), HostImportOffset(*segment.import, begin), end - begin}); + } + if (!copies.empty()) copyBuffer(commands, scratch->Handle(), segment.import->buffer, static_cast(copies.size()), copies.data()); + } + if (!saveUnits.empty()) { + const auto saved = savedBytes->Bytes(); + for (const auto unit : saveUnits) { + const auto offset = static_cast(layerBegin(unit) - descriptor.baseAddress); + std::memcpy(saved.data() + offset, original.data() + offset, static_cast(layerBytes(unit))); + } + std::vector saves; + for (const auto& [from, to] : stores) saves.push_back({scratchOffset(from), from - descriptor.baseAddress, to - from}); + copyBuffer(commands, scratch->Handle(), savedBytes->Handle(), static_cast(saves.size()), saves.data()); + } + VkImageMemoryBarrier backToGeneral = toSource; + backToGeneral.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + backToGeneral.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; + backToGeneral.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + backToGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; + const VkMemoryBarrier stored{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, 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 | VK_ACCESS_HOST_READ_BIT}; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &stored, 0, nullptr, 1, &backToGeneral); + if (batch) { + batch->SubmitAndWait(); + return storedBytes; + } + recorder->EndGpuTiming(timing, storedBytes); + recorder->MarkShaderReadsCovered(); + recorder->NotePendingWrite(hullBegin, hullBytes); + noteStore(recorder->LastWriteNote(hullBegin, hullBytes)); + return storedBytes; +} + void StorageTexture::Flush() { { auto& live = Live(); @@ -3914,13 +4176,38 @@ void waitForIdlePublish(const char* reason) { bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except, const char* reason, PublishScope scope, bool* published, bool waited) { static_cast(FlushDepthPlanes(address, bytes)); if (published != nullptr) *published = false; + const auto nothingToStore = [&] { + auto& pending = Pending(); + if (pending.flushingCount.load(std::memory_order_acquire) != 0) return false; + if (!pending.textures.SurelyClear(address, bytes)) { + std::lock_guard lock(pending.mutex); + const auto* exempt = refreshing; + if (pending.textures.MayOverlap(address, bytes) && std::any_of(pending.textures.begin(), pending.textures.end(), [&](const StorageTexture* texture) { return texture != except && texture != exempt && texture->overlaps(address, bytes) && texture->pendingUnitInside(address, bytes); })) return false; + if (pending.flushingCount.load(std::memory_order_acquire) != 0) return false; + } + return scope == PublishScope::None || !AnyShadowedOverlaps(address, bytes); + }; + if (nothingToStore()) return false; if (AgcDriver::DriverDetail::DrawPipeline::HeldReads(address, bytes)) { queuedReadHeld.fetch_add(1, std::memory_order_relaxed); return false; } + std::unique_lock drawnHold; + if (auto& drawnPending = Pending(); drawnPending.drawnCount.load(std::memory_order_acquire) != 0 && !GuestMemory::GpuMutex().HeldByThisThread()) { + bool overlap = false; + { + std::lock_guard lock(drawnPending.mutex); + overlap = std::any_of(drawnPending.drawn.begin(), drawnPending.drawn.end(), [&](const PendingWrites::Drawn& entry) { return entry.texture->dirty && entry.texture->overlaps(address, bytes); }); + } + if (overlap) { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); + drawnHold = std::unique_lock(GuestMemory::GpuMutex()); + } + } // The images stay alive across the scan: their last owner may be a batch's kept list, which // another thread releases outside the GPU mutex once the batch completed. std::vector> flush; + std::vector ownNotes; { auto& pending = Pending(); std::lock_guard lock(pending.mutex); @@ -3948,8 +4235,12 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons for (const auto& texture : flush) pending.flushing.push_back(texture.get()); pending.flushingCount.store(pending.flushing.size(), std::memory_order_release); if (!flush.empty()) BumpPendingSerial(); + for (const auto& texture : flush) { + if (texture->drawnListed) ownNotes.insert(ownNotes.end(), texture->storeNotes.begin(), texture->storeNotes.end()); + } } if (flush.empty()) { + if (drawnHold.owns_lock()) drawnHold.unlock(); const bool stored = WaitForFlushing(address, bytes, except); // Units shadowed over the range (another image's results retiled into the import's unit // shadow, or an earlier flush's) still reach the import as the scope asks. @@ -3973,10 +4264,11 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons for (const auto& texture : flush) std::fprintf(stderr, "[flush] image 0x%llx+0x%llx for %s 0x%llx+0x%zx\n", static_cast(texture->descriptor.baseAddress), static_cast(texture->guestBytes), reason, static_cast(address), bytes); } std::lock_guard gpu(GuestMemory::GpuMutex()); - if (auto* recorder = Recorder::Active(); recorder != nullptr && !Recorder::InCompletion() && recorder->PendingWriterSerial(address, bytes) != 0) { + if (!ownNotes.empty() && (flush.size() != 1 || std::any_of(flush.begin(), flush.end(), [](const std::shared_ptr& texture) { return ExistingHostImportFor(texture->context, texture->descriptor.baseAddress, static_cast(texture->guestBytes)) == nullptr; }))) ownNotes.clear(); + if (auto* recorder = Recorder::Active(); recorder != nullptr && !Recorder::InCompletion() && recorder->PendingWriterSerial(address, bytes, ownNotes) != 0) { try { recorder->Reap(); - if (const auto writer = recorder->PendingWriterSerial(address, bytes); writer == recorder->OpenSerial()) { + if (const auto writer = recorder->PendingWriterSerial(address, bytes, ownNotes); writer == recorder->OpenSerial()) { Recorder::CountSync(recorder->OpenWriteOverlaps(address, bytes) ? 1 : 3); recorder->Sync(); } else if (writer != 0) { @@ -4086,7 +4378,8 @@ void StorageTexture::saveBeforeUnwatch() { const auto end = begin + layerBytes(unit); const auto spanBegin = begin & ~(block - 1); const auto spanBlocks = static_cast((end - spanBegin + block - 1) / block); - const std::vector generations(spanBlocks, layerGeneration[unit]); + std::vector generations(spanBlocks); + for (std::size_t k = 0; k < spanBlocks; ++k) generations[k] = unitBlockGeneration(unit, spanBegin + k * block); std::vector changedBlocks(spanBlocks); if (!GuestMemory::ChangedBlocks(begin, static_cast(end - begin), generations, changedBlocks)) continue; forgetKeptAtUnwatch(unit); @@ -4094,7 +4387,7 @@ void StorageTexture::saveBeforeUnwatch() { 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), layerGeneration[unit])) && !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))) && !unchangedSinceBaseline(from, to)) keptAtUnwatch.emplace(from, layerGeneration[unit]); } layerOriginal[unit] = true; cancelStoredSave(unit); @@ -4258,6 +4551,49 @@ void StorageTexture::StoreReadbackBeforeLabel(const char* reason) { for (const auto& [address, bytes] : ranges) static_cast(FlushPending(address, bytes, nullptr, reason)); } +std::size_t StorageTexture::StoreDrawnLinear(const char* reason) { + auto& pending = Pending(); + if (pending.drawnCount.load(std::memory_order_acquire) == 0 || Recorder::InCompletion()) return 0; + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); + std::lock_guard gpu(GuestMemory::GpuMutex()); + std::vector> ranges; + { + std::lock_guard lock(pending.mutex); + for (const auto& entry : pending.drawn) { + if (entry.texture->dirty) ranges.emplace_back(entry.texture->descriptor.baseAddress, static_cast(entry.texture->guestBytes)); + } + } + std::size_t stored = 0; + for (const auto& [address, bytes] : ranges) { + if (FlushPending(address, bytes, nullptr, reason)) ++stored; + } + return stored; +} + +std::size_t StorageTexture::StoreDrawnLinearTouching(const char* reason, const std::function& touches, bool all) { + auto& pending = Pending(); + if (pending.drawnCount.load(std::memory_order_acquire) == 0 || Recorder::InCompletion()) return 0; + std::vector> ranges; + { + std::lock_guard lock(pending.mutex); + for (const auto& entry : pending.drawn) { + if (!entry.texture->dirty) continue; + const auto begin = entry.texture->descriptor.baseAddress; + const auto end = begin + entry.texture->guestBytes; + if (!all && !touches(begin, end)) continue; + ranges.emplace_back(begin, static_cast(entry.texture->guestBytes)); + } + } + if (ranges.empty()) return 0; + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); + std::lock_guard gpu(GuestMemory::GpuMutex()); + std::size_t stored = 0; + for (const auto& [address, bytes] : ranges) { + if (FlushPending(address, bytes, nullptr, reason)) ++stored; + } + return stored; +} + bool StorageTexture::AnyPending() { auto& pending = Pending(); std::lock_guard lock(pending.mutex); @@ -4324,37 +4660,96 @@ bool PendingStorageOverlaps(std::uint64_t address, std::size_t bytes, const Stor } std::uint64_t StorageTexture::StampKeepingNewerResults(std::uint64_t address, std::size_t bytes, std::uint64_t recorded, const std::function& stamp) { - std::vector, std::uint32_t>> kept; + struct Candidate { + std::shared_ptr texture; + std::uint32_t unit; + bool drawn; + }; + struct Kept { + std::shared_ptr texture; + std::uint32_t unit; + std::uint64_t from; + std::uint64_t to; + std::uint64_t since; + bool whole; + bool drawn; + }; + std::vector candidates; if (recorded != 0 && bytes != 0) { auto& pending = Pending(); std::lock_guard lock(pending.mutex); - for (auto* texture : pending.textures) { - if (!texture->blockUnits || !texture->overlaps(address, bytes)) continue; + const auto consider = [&](StorageTexture* texture) { + if ((!texture->blockUnits && !texture->drawnListed) || !texture->overlaps(address, bytes)) return; auto alive = texture->weak_from_this().lock(); - if (alive == nullptr) continue; + if (alive == nullptr) return; for (std::uint32_t unit = 0; unit < texture->trackedLayers; ++unit) { const auto begin = texture->layerBegin(unit); const auto unitBytes = texture->layerBytes(unit); if (!texture->layerPending[unit] || begin >= address + bytes || address >= begin + unitBytes) continue; - if (texture->layerGeneration[unit] >= recorded) kept.emplace_back(alive, unit); + if (texture->layerGeneration[unit] >= recorded) candidates.push_back({alive, unit, texture->drawnListed}); } + }; + for (auto* texture : pending.textures) consider(texture); + for (auto* texture : pending.flushing) consider(texture); + } + constexpr std::uint64_t block = 65536; + std::vector kept; + for (const auto& [texture, unit, drawn] : candidates) { + const auto begin = texture->layerBegin(unit); + const auto end = begin + texture->layerBytes(unit); + GuestMemory::CollectWritesUncached(begin, static_cast(end - begin)); + if (GuestMemory::UnchangedSince(begin, static_cast(end - begin), texture->layerGeneration[unit])) { + kept.push_back({texture, unit, begin, end, texture->layerGeneration[unit], true, drawn}); + continue; + } + if (!drawn) continue; + const auto first = std::max(begin, address) / block; + const auto last = (std::min(end, address + bytes) - 1) / block; + const auto from = std::max(begin, first * block); + const auto to = std::min(end, (last + 1) * block); + std::vector generations(static_cast(last - first + 1)); + for (std::size_t k = 0; k < generations.size(); ++k) generations[k] = texture->unitBlockGeneration(unit, (first + k) * block); + std::vector changed(generations.size()); + if (!GuestMemory::ChangedBlocks(from, static_cast(to - from), generations, changed)) continue; + for (std::size_t k = 0; k < generations.size(); ++k) { + const auto at = (first + k) * block; + const auto blockFrom = std::max(at, begin); + const auto blockTo = std::min(at + block, end); + const bool edge = blockFrom != at || blockTo != at + block; + if (changed[k] == GuestMemory::BlockWritten && (!edge || !GuestMemory::UnchangedSince(blockFrom, static_cast(blockTo - blockFrom), generations[k]))) continue; + if (!kept.empty() && !kept.back().whole && kept.back().texture == texture && kept.back().unit == unit && kept.back().since == generations[k] && kept.back().to == blockFrom) kept.back().to = blockTo; + else kept.push_back({texture, unit, blockFrom, blockTo, generations[k], false, true}); } } - std::erase_if(kept, [](const auto& entry) { - const auto& [texture, unit] = entry; - const auto begin = texture->layerBegin(unit); - const auto unitBytes = static_cast(texture->layerBytes(unit)); - GuestMemory::CollectWritesUncached(begin, unitBytes); - return !GuestMemory::UnchangedSince(begin, unitBytes, texture->layerGeneration[unit]); - }); const auto generation = stamp(); if (generation == 0 || kept.empty()) return generation; + std::vector confirmed; + for (const auto& entry : kept) { + if ((entry.whole && !entry.drawn) || GuestMemory::UnchangedSinceCollected(entry.from, static_cast(entry.to - entry.from), entry.since)) { + confirmed.push_back(entry); + continue; + } + if (entry.from / block == (entry.to - 1) / block) continue; + const auto last = (std::min(entry.to, address + bytes) - 1) / block; + for (auto at = std::max(entry.from, address) / block * block; at <= last * block; at += block) { + const auto from = std::max(at, entry.from); + const auto to = std::min(at + block, entry.to); + if (GuestMemory::UnchangedSinceCollected(from, static_cast(to - from), entry.since)) confirmed.push_back({entry.texture, entry.unit, from, to, entry.since, false, true}); + } + } auto& pending = Pending(); std::lock_guard lock(pending.mutex); - for (const auto& [texture, unit] : kept) { - if (!texture->layerPending[unit]) continue; - texture->layerGeneration[unit] = generation; - texture->refreshGeneration(); + for (const auto& entry : confirmed) { + auto& texture = *entry.texture; + if (!texture.layerPending[entry.unit]) continue; + if (entry.whole) { + texture.layerGeneration[entry.unit] = generation; + texture.refreshGeneration(); + continue; + } + const auto spanBegin = texture.descriptor.baseAddress / block; + if (texture.completionKept.empty()) texture.completionKept.assign(static_cast((texture.descriptor.baseAddress + texture.guestBytes - 1) / block - spanBegin + 1), 0); + for (auto at = entry.from / block; at <= (entry.to - 1) / block; ++at) texture.completionKept[static_cast(at - spanBegin)] = generation; } return generation; } @@ -4470,6 +4865,7 @@ bool StorageTexture::AccessKeptByCpu(std::uint64_t address, std::size_t bytes, s // store there would see, and the stamps only grow. GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Hook); std::lock_guard gpu(GuestMemory::GpuMutex()); + if (std::any_of(overlapping.begin(), overlapping.end(), [](const std::shared_ptr& texture) { return texture->drawnArea().drawn; })) return false; if (!blocksKept(overlapping, address, bytes)) return false; const auto site = static_cast(GuestMemory::CurrentReadSite()); std::size_t dropped = 0; @@ -4601,6 +4997,7 @@ std::size_t StorageTexture::DiscardPendingInside(std::uint64_t address, std::siz texture->layerPending.assign(texture->trackedLayers, false); texture->layerOriginal.assign(texture->trackedLayers, false); texture->cancelStoredSaves(); + texture->forgetDrawn(); it = pending.textures.erase(it); ++discarded; } else if (texture != exempt && texture->blockUnits && texture->overlaps(address, bytes)) { @@ -5210,6 +5607,12 @@ void StorageTexture::blockGenerations(std::vector& generations) c } } +std::uint64_t StorageTexture::unitBlockGeneration(std::uint32_t unit, std::uint64_t at) const { + const auto unitGeneration = layerGeneration[unit]; + if (unitGeneration == 0 || !layerPending[unit] || completionKept.empty()) return unitGeneration; + return std::max(unitGeneration, completionKept[static_cast(at / 65536 - descriptor.baseAddress / 65536)]); +} + namespace { std::atomic& ClearHandOverOn() { @@ -5506,10 +5909,12 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { if (!layers[layer]) continue; const auto end = layerBegin(layer) + layerBytes(layer); - for (auto at = layerBegin(layer) & ~(block - 1); at < end; at += block) generations[static_cast((at - spanBegin) / block)] = layerGeneration[layer]; + for (auto at = layerBegin(layer) & ~(block - 1); at < end; at += block) generations[static_cast((at - spanBegin) / block)] = unitBlockGeneration(layer, at); } + const auto drawn = drawnArea(); std::vector changedBlocks(spanBlocks); - const bool tracked = GuestMemory::ChangedBlocks(firstStored, static_cast(lastStored - firstStored), generations, changedBlocks); + std::vector cpuBlocks(drawn.drawn ? spanBlocks : 0); + const bool tracked = GuestMemory::ChangedBlocks(firstStored, static_cast(lastStored - firstStored), generations, changedBlocks, cpuBlocks); std::vector compared(trackedLayers, false); if (!tracked) { awaitStoredSave(&layers); @@ -5534,7 +5939,8 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { if (!layers[layer]) continue; const auto begin = layerBegin(layer); const auto end = begin + layerBytes(layer); - if (tracked ? layerGeneration[layer] == 0 : !compared[layer]) { + const bool unknown = tracked ? layerGeneration[layer] == 0 : !compared[layer]; + if (unknown && !drawn.rect) { unsavedOutsideWatch = unsavedOutsideWatch || !tracked; keep.emplace_back(begin, end); continue; @@ -5544,12 +5950,16 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { const auto to = std::min(at + block, end); if (from >= to) continue; const bool edge = from != at || to != at + block; - if (keptAtUnwatchHolds(from, layer) || (changedBlocks[static_cast((at - spanBegin) / block)] == GuestMemory::BlockWritten && (compared[layer] || !edge || GuestMemory::StoredOver(from, static_cast(to - from), layerGeneration[layer])) && !unchangedSinceBaseline(from, to))) { + 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)); + if (written && (!drawn.rect || (tracked && !kept && !unknown && cpuBlocks[index] == 0))) { skippedAny = true; skippedLayer[layer] = true; ++skipped; continue; } + if (written && drawn.rect) skippedLayer[layer] = true; if (!keep.empty() && keep.back().second == from) keep.back().second = to; else keep.emplace_back(from, to); } @@ -5563,10 +5973,15 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { const auto first = keep.empty() ? descriptor.baseAddress + guestBytes : keep.front().first; std::fprintf(stderr, "[flush] image 0x%llx+0x%llx keeps %zu CPU-written 64 KiB blocks (first stored byte at +0x%llx, %zu ranges stored, generation %llu)\n", static_cast(descriptor.baseAddress), static_cast(guestBytes), skipped, static_cast(first - descriptor.baseAddress), keep.size(), static_cast(generation)); } + std::vector> spans; + if (drawn.rect) spans = drawnSpans(drawn, keep); + const auto& stores = drawn.rect ? spans : keep; + const auto stampBegin = drawn.rect && !stores.empty() ? stores.front().first : firstStored; + const auto stampEnd = drawn.rect && !stores.empty() ? stores.back().second : lastStored; // Taken before this write-back stamps anything (see advanceAdjacent), with the 64 KiB block span // its stamps cover. - const auto firstBlock = firstStored / block; - const auto lastBlock = (lastStored - 1) / block; + const auto firstBlock = stampBegin / block; + const auto lastBlock = (stampEnd - 1) / block; const auto adjacent = adjacentPendingUnchanged(firstBlock, lastBlock); // After the store: the stored layers are in sync at a fresh generation unless blocks were kept // for the CPU (then the layer is stale there, keeps its old generation and re-uploads at its @@ -5603,7 +6018,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { advanceAdjacent(adjacent, now, firstBlock, lastBlock); if (!shadowed.empty()) markShadowed(now); }; - if (keep.empty()) { + if (stores.empty()) { // Every selected block was written by the CPU since the layer's generation: nothing to // store, the keys stay as they are, and the image is stale there (re-uploaded at its next // use, never from a matching `original`). @@ -5633,6 +6048,24 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { ++Profile().storageDirectWriteBacks; return; } + if (drawn.rect) { + PublishShadow(stampBegin, static_cast(stampEnd - stampBegin), PublishScope::PartialUnits, PublishReason::Upload); + const auto storedBytes = storeDrawnRect(drawn, segments, stores, saveUnits); + countStorageWriteBack(storedBytes, true, true); + if (profile) Profile().storageGpu += timer.lap(); + forgetStored(); + uploadedKeys = DccKeys::Uncompressed; + std::vector> stamps; + for (const auto& [from, to] : stores) { + if (!stamps.empty() && from / block <= (stamps.back().second - 1) / block + 1) stamps.back().second = to; + else stamps.emplace_back(from, to); + } + for (const auto& [from, to] : stamps) GuestMemory::MarkWritten(from, static_cast(to - from)); + settle(true); + commitStoredSave(saveUnits); + ++Profile().storageDirectWriteBacks; + return; + } // The whole-layer copies below overwrite the interior units of the span; the edge units // keep a neighbour's shadowed results in the rest of their bytes, which reach the import // first (this write-back's stamps then make those units stale). @@ -5735,8 +6168,9 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { recorder->EndGpuTiming(timing, storedBytes); recorder->MarkShaderReadsCovered(); recorder->NotePendingWrite(firstStored, static_cast(lastStored - firstStored)); + if (drawn.drawn) noteStore(recorder->LastWriteNote(firstStored, static_cast(lastStored - firstStored))); } - countStorageWriteBack(storedBytes, true); + countStorageWriteBack(storedBytes, true, drawn.drawn); if (profile) Profile().storageGpu += timer.lap(); // The guest bytes now differ from `original`; other caches of the range see the write. forgetStored(); @@ -5777,10 +6211,10 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { overhang = covered >= end; } } - const bool registered = std::all_of(keep.begin(), keep.end(), [](const auto& range) { return RegisteredReadableCovers(range.first, static_cast(range.second - range.first)); }); + const bool registered = std::all_of(stores.begin(), stores.end(), [](const auto& range) { return RegisteredReadableCovers(range.first, static_cast(range.second - range.first)); }); if (unregisteredDrop && !overhang && !registered) { static std::atomic reports{0}; - if (reports.fetch_add(1) < 4) std::fprintf(stderr, "[gpu] storage image 0x%llx+0x%llx: memory no longer registered; %zu pending ranges dropped\n", static_cast(descriptor.baseAddress), static_cast(guestBytes), keep.size()); + if (reports.fetch_add(1) < 4) std::fprintf(stderr, "[gpu] storage image 0x%llx+0x%llx: memory no longer registered; %zu pending ranges dropped\n", static_cast(descriptor.baseAddress), static_cast(guestBytes), stores.size()); unregisteredDropped.fetch_add(1, std::memory_order_relaxed); forgetStored(); for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { @@ -5797,6 +6231,10 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { std::vector unsaved = layerOriginal; unsaved.flip(); for (const auto& [begin, end] : unitRuns(unsaved)) { + if (drawn.rect) { + GuestMemory::ReadCommitted(descriptor.baseAddress + begin, std::span(original).subspan(static_cast(begin), static_cast(end - begin))); + continue; + } for (const auto& [from, to] : keep) { const auto first = std::max(descriptor.baseAddress + begin, from); const auto last = std::min(descriptor.baseAddress + end, to); @@ -5858,7 +6296,9 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { APS5_LOG_CHARS_OUT("StorageTexture writeback submit"); batch.SubmitAndWait(); APS5_LOG_CHARS_OUT("StorageTexture writeback done"); - countStorageWriteBack(guestBytes, false); + std::uint64_t storedBytes = 0; + for (const auto& [from, to] : stores) storedBytes += to - from; + countStorageWriteBack(drawn.rect ? storedBytes : guestBytes, false, drawn.drawn); if (profile) Profile().storageGpu += timer.lap(); if (dump) { char name[64]; @@ -5873,7 +6313,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { // Store only the kept ranges' blocks that changed, so concurrent CPU writes to untouched texels // survive; `original` follows for the stored layers (the others' guest bytes are unchanged). const auto current = host.Bytes(); - for (const auto& [from, to] : keep) { + for (const auto& [from, to] : stores) { const auto offset = static_cast(from - descriptor.baseAddress); const auto length = static_cast(to - from); GuestMemory::WriteChangedCommitted(from, current.subspan(offset, length), std::span(original).subspan(offset, length)); @@ -5909,6 +6349,7 @@ StorageTexture::~StorageTexture() { { auto& pending = Pending(); std::lock_guard lock(pending.mutex); + forgetDrawn(); if (dirty) { dirty = false; pending.textures.remove(this); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp index e667909a5b..1756e2fa02 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp @@ -99,7 +99,7 @@ constexpr auto MakeFormatLookupTable() { constexpr auto kFormatLookupTable = MakeFormatLookupTable(); std::uint32_t remapGuestFormat(std::uint32_t guestFormat) { - return guestFormat == 30 || guestFormat == 34 || guestFormat == 43 ? 20 : guestFormat; + return guestFormat == 30 || guestFormat == 34 || guestFormat == 36 || guestFormat == 43 ? 20 : guestFormat; } const FormatEntry& findFormatEntry(std::uint32_t guestFormat) { @@ -115,6 +115,11 @@ VkFormat ResolveTextureFormat(std::uint32_t guestFormat) { return findFormatEntry(guestFormat).vkFormat; } +VkFormat DccClearTextureFormat(std::uint32_t guestFormat) { + if (IsRepackedTextureFormat(guestFormat) || guestFormat == static_cast(ShaderRecompiler::IrBufferFormat::Format11_11_10Float)) return VK_FORMAT_B10G11R11_UFLOAT_PACK32; + return ResolveTextureFormat(guestFormat); +} + std::uint32_t SrgbDecodeFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical) { const char* forced = std::getenv("APS5_SRGB_SHADER_DECODE"); const bool always = forced != nullptr && (std::strcmp(forced, "1") == 0 || std::strcmp(forced, "copy") == 0); @@ -131,7 +136,7 @@ std::uint32_t SrgbDecodeFormats(PFN_vkGetPhysicalDeviceFormatProperties formatPr } VkFormat SampledTextureFormat(const Context& context, std::uint32_t guestFormat) { - if (IsRepackedTextureFormat(guestFormat)) return VK_FORMAT_B10G11R11_UFLOAT_PACK32; + if (IsRepackedTextureFormat(guestFormat) || guestFormat == static_cast(ShaderRecompiler::IrBufferFormat::Format11_11_10Float)) return VK_FORMAT_B10G11R11_UFLOAT_PACK32; const auto format = static_cast(guestFormat); if ((context.srgbDecodeFormats & ShaderRecompiler::SrgbDecodeBit(format)) == 0u) return ResolveTextureFormat(guestFormat); return ResolveTextureFormat(static_cast(ShaderRecompiler::SrgbUnormFormat(format))); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp index ff1bed0787..494c38359e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp @@ -466,23 +466,26 @@ bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t leve return true; } -GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed) { +GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed, bool keysReadAsStored) { const bool layered = viewed.dimension == TextureDimension::k2DArray || viewed.dimension == TextureDimension::k1DArray || viewed.dimension == TextureDimension::kCube; - if (!layered || viewed.baseArray == 0 || viewed.baseArray > viewed.depthOrLastArray || viewed.dccAddress != 0) return viewed; + if (!layered || viewed.baseArray == 0 || viewed.baseArray > viewed.depthOrLastArray || (viewed.dccAddress != 0 && !keysReadAsStored)) return viewed; + std::uint32_t sliceMask = 0; if (const auto xorMode = XorSwizzleMode(viewed.tileMode); xorMode != 0u) { const auto* equation = FindTextureSwizzleEquation(xorMode, BytesPerElement(viewed.format)); if (equation == nullptr) return viewed; std::uint32_t sliceBits = 0; for (const auto bit : equation->bits) sliceBits |= bit >> 24u; - const auto sliceMask = sliceBits == 0 ? 0u : (1u << std::bit_width(sliceBits)) - 1u; - if ((viewed.baseArray & sliceMask) != 0) return viewed; + sliceMask = sliceBits == 0 ? 0u : (1u << std::bit_width(sliceBits)) - 1u; } + const auto first = viewed.baseArray & ~sliceMask; + if (first == 0) return viewed; const auto geometry = DescribeSurface(viewed); if (geometry.thick || geometry.layers != viewed.depthOrLastArray + 1u) return viewed; auto window = viewed; - window.baseAddress = viewed.baseAddress + geometry.GuestLayerOffset(viewed.baseArray); - window.depthOrLastArray = viewed.depthOrLastArray - viewed.baseArray; - window.baseArray = 0; + window.baseAddress = viewed.baseAddress + geometry.GuestLayerOffset(first); + window.depthOrLastArray = viewed.depthOrLastArray - first; + window.baseArray = viewed.baseArray - first; + window.dccAddress = 0; return window; } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index c2c44c6aed..37e518eb51 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -2360,6 +2360,64 @@ void importedHeapMirrorTests(const Context& context, const BdaTestAccess& access GuestArena::GuestArenaRelease_nid_postfix(block, bytes); } +void retiredImportWatchTests(const Context& context) { +#ifdef _WIN32 + if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) { + std::cout << "guest arena unavailable or not write-watched: the write watch of retired imports not tested\n"; + return; + } + constexpr std::size_t piece = 65536; + constexpr std::size_t bytes = 2 * piece; + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, bytes); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); + std::memset(block, 0x5a, bytes); + auto* guest = static_cast(block); + const auto address = reinterpret_cast(block); + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(bytes); + SetHostImportDeferredSettles(false); + ClearHostImports(importing.device); + const auto decided = PrepareImportWatch(importing); + SetImportWatch(importing, ImportWatch::Unwatch); + const auto registry = [&](std::size_t offset, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, guest + offset, piece, true, true, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, guest + offset); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + const auto build = [&] { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.Upload(true); + leased.WriteBack(); + }; + registry(0, true); + build(); + Require(HostImportCovers(importing, address, piece), "the first 64 KiB piece was not imported on its own"); + Require(!AgcDriver::GuestMemory::Watched(address, piece), "an unwatched 64 KiB import kept its piece in the write watch"); + registry(piece, true); + SetHostImportBudget(4096); + const auto before = MirrorCounters(); + build(); + Require(!HostImportCovers(importing, address, piece), "the piece's import outlived the run that grew past it, or a 128 KiB block fit a 4 KiB budget"); + Require(AgcDriver::GuestMemory::Watched(address, piece), "a 64 KiB piece whose import retired when its run grew into a block stayed outside the write watch"); + Require(MirrorCounters().heapMirrors == before.heapMirrors + 2, "a 64 KiB piece whose import retired when its run grew into a block was copied, not heap mirrored, once no import fit"); + registry(piece, false); + registry(0, false); + build(); + SetImportWatch(importing, decided); + ClearHostImports(importing.device); + SetHostImportBudget(0); + SetHostImportWindow(0); + SetHostImportDeferredSettles(true); + GuestArena::GuestArenaReset_nid_postfix(block, bytes); + GuestArena::GuestArenaRelease_nid_postfix(block, bytes); +#else + static_cast(context); +#endif +} + void importedFreshTests(const Context& context) { #ifndef _WIN32 if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { @@ -3430,6 +3488,137 @@ void RunHeapMirrorRunUnwatchTests(const Context& context, const BdaTestAccess& a std::cout << "an address-based build refreshes the unwatched heap of a run in full and its watched neighbour by its writes\n"; } +void RunAddressCopyCapTests(const Context& context, const BdaTestAccess& access, bool remembered) { + constexpr std::size_t block = 65536; + constexpr std::size_t heapBytes = 32 * block; + constexpr std::size_t idleBytes = 16 * block; + constexpr std::size_t busyBytes = block; +#ifdef _WIN32 + if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) { + std::cout << "guest arena unavailable or not write-watched: the address copy cap not tested\n"; + return; + } +#else + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { + std::cout << "write watch unavailable: the address copy cap not tested\n"; + return; + } +#endif + struct Piece { + void* pointer = nullptr; + void* mapping = nullptr; + std::size_t allocated = 0; + std::size_t bytes = 0; + }; + std::array pieces{}; + const std::array, 4> sizes{{{heapBytes + block, heapBytes}, {heapBytes + block, idleBytes}, {busyBytes + block, busyBytes}, {busyBytes + block, busyBytes}}}; + for (std::size_t index = 0; index < pieces.size(); ++index) { + auto& piece = pieces[index]; + piece.allocated = sizes[index].first; + piece.bytes = sizes[index].second; +#ifdef _WIN32 + piece.pointer = GuestArena::GuestArenaAllocate_nid_postfix(piece.allocated, block); + Require(piece.pointer != nullptr, "cannot allocate the address copy cap ranges in the guest arena"); + GuestArena::GuestArenaCommit_nid_postfix(piece.pointer, piece.allocated, PAGE_READWRITE, piece.allocated); + piece.mapping = piece.pointer; +#else + piece.mapping = mmap(nullptr, piece.allocated + block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(piece.mapping != MAP_FAILED, "cannot map the address copy cap ranges"); + piece.pointer = reinterpret_cast((reinterpret_cast(piece.mapping) + block - 1) & ~std::uintptr_t{block - 1}); +#endif + std::memset(piece.pointer, 0x5a, piece.allocated); + } + if (pieces[1].pointer < pieces[0].pointer) { + std::swap(pieces[0].pointer, pieces[1].pointer); + std::swap(pieces[0].mapping, pieces[1].mapping); + } + const auto heap = reinterpret_cast(pieces[0].pointer); + const auto idle = reinterpret_cast(pieces[1].pointer); + const auto busy = reinterpret_cast(pieces[2].pointer); + Require(heap < idle, "the idle range does not follow the heap in the registry"); +#ifndef _WIN32 + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(pieces[0].pointer, heapBytes); +#endif + const auto registry = [&](bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + for (std::size_t index = 0; index < 3; ++index) { + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, pieces[index].pointer, pieces[index].bytes, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, pieces[index].pointer); + } + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + const auto unrelated = [&](bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, pieces[3].pointer, pieces[3].bytes, true, false, false); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, pieces[3].pointer); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + registry(true); +#ifdef _WIN32 + AgcDriver::GuestMemory::Unwatch(idle, idleBytes); + AgcDriver::GuestMemory::Unwatch(busy, busyBytes); +#endif + auto importing = context; + importing.hostImportAlignment = block; + SetHostImportBudget(heapBytes + idleBytes + busyBytes - block); + Require(HostImportFor(importing, idle, 64) != nullptr && HostImportFor(importing, busy, 64) != nullptr, "the idle and busy ranges were not imported"); + Require(HostImportLiveBytes() == idleBytes + busyBytes && !HostImportCovers(importing, heap, heapBytes), "the imports before the build are not the idle and busy ranges"); + Require(AgcDriver::GuestMemory::Watched(heap, heapBytes), "the heap is not write-watched"); + const auto before = MirrorCounters(); + const auto builds = [] { return HostImportCounters().requesters[static_cast(ImportRequester::Build)]; }; + const auto unwatch = [&] { + AgcDriver::GuestMemory::Unwatch(heap + heapBytes / 2, block); + Require(AgcDriver::GuestMemory::Watched(heap, heapBytes / 2) && !AgcDriver::GuestMemory::Watched(heap, heapBytes), "the heap is not partly outside the write watch"); + }; + if (remembered) { + unwatch(); + SetAddressCopyLimit(2 * heapBytes); + { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.Upload(true); + Require(MirrorCounters().heapMirrors == before.heapMirrors && !HostImportCovers(importing, heap, heapBytes), "the first build did not copy the heap whose import the budget refused"); + leased.WriteBack(); + } + SetAddressCopyLimit(0); + unrelated(true); + } + for (int use = 0; use < 300; ++use) Require(HostImportFor(importing, busy, 64) != nullptr, "the busy range lost its import"); + if (!remembered) unwatch(); + const auto refused = builds(); + { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.Upload(true); + const bool imported = HostImportCovers(importing, heap, heapBytes); + Require(imported || (!remembered && MirrorCounters().heapMirrors == before.heapMirrors + 1), "the heap is neither imported nor mirrored"); + const auto ranges = leased.AddressRanges(); + const auto found = std::find_if(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin == heap; }); + Require(found != ranges.end() && found->deviceAddress != 0u, "the heap is missing from the BDA table"); + Require(imported || access.addressBytes(found->deviceAddress)[heapBytes / 2 + 3] == std::byte{0x5a}, "the heap mirror does not hold the guest bytes"); + leased.WriteBack(); + } + Require(!remembered || builds().remembered == refused.remembered + 1, "the build under the copy cap was not answered from the refusal the first build remembered"); + SetHostImportBudget(0); + if (remembered) unrelated(false); + registry(false); + ClearImageMirrors(importing.device); + ClearHostImports(importing.device); + ClearSnapshotCopies(importing.device); +#ifndef _WIN32 + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(pieces[0].pointer, heapBytes); +#endif + for (const auto& piece : pieces) { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(piece.pointer, piece.allocated); + GuestArena::GuestArenaRelease_nid_postfix(piece.pointer, piece.allocated); +#else + munmap(piece.mapping, piece.allocated + block); +#endif + } + std::cout << (remembered ? "an address-based build answered from a remembered budget refusal evicts instead of copying a partly unwatched heap past the cap\n" : "an address-based build serves a partly unwatched heap whose import is over the budget\n"); +} + void hostWriteWatchExclusionTests() { #ifdef _WIN32 std::vector host(64, 0x31); @@ -3443,6 +3632,200 @@ void hostWriteWatchExclusionTests() { #endif } +void sparseJoinedHeapMirrorTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t half = 1u << 20u; + constexpr std::size_t reserved = 2 * half; + constexpr std::size_t page = 65536; +#ifdef _WIN32 + if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) { + std::cout << "guest arena unavailable or not write-watched: sparse heap mirrors over adjacent ranges not tested\n"; + return; + } + void* block = GuestArena::GuestArenaAllocate_nid_postfix(reserved, page); + auto* guest = static_cast(block); + GuestArena::GuestArenaCommit_nid_postfix(guest, page, PAGE_READWRITE, page); + GuestArena::GuestArenaCommit_nid_postfix(guest + half, page, PAGE_READWRITE, page); +#else + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { + std::cout << "write watch unavailable: sparse heap mirrors over adjacent ranges not tested\n"; + return; + } + void* mapping = mmap(nullptr, reserved + page, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(mapping != MAP_FAILED, "cannot reserve the sparse heap over adjacent ranges"); + auto* guest = reinterpret_cast((reinterpret_cast(mapping) + page - 1) & ~std::uintptr_t{page - 1}); + Require(mprotect(guest, page, PROT_READ | PROT_WRITE) == 0 && mprotect(guest + half, page, PROT_READ | PROT_WRITE) == 0, "cannot commit the sparse heap pages"); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(guest, reserved); + void* block = guest; +#endif + auto* second = guest + half; + std::memset(guest, 0x5a, page); + std::memset(second, 0x6b, page); + const auto base = reinterpret_cast(block); + const auto registry = [&](bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) { + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, guest, half, true, true, true); + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, second, half, true, true, true); + } else { + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, guest); + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, second); + } + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + const auto sweep = [&] { + GuestBufferMemory memory(context); + memory.AcquireRegistered(); + memory.Upload(true); + memory.WriteBack(); + }; + sweep(); + registry(true); + const auto bind = [&] { + GuestBufferMemory memory(context); + memory.AddWritable(base, reserved); + memory.Upload(false); + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(base, reserved, adjustment); + Require(adjustment == 0, "a sparse heap view over adjacent ranges is adjusted"); + const auto bytes = access.bytes(view.buffer); + Require(view.range >= half + page && bytes[view.offset + page - 1] == std::byte{0x5a} && bytes[view.offset + page] == std::byte{} && bytes[view.offset + half] == std::byte{0x6b}, "a sparse heap over adjacent ranges is not bound to its committed extent"); + bytes[view.offset + half + 8] = std::byte{0x77}; + memory.WriteBack(); + return view; + }; + const auto before = MirrorCounters(); + const auto first = bind(); + const auto made = MirrorCounters(); + Require(made.sparseBinds == before.sparseBinds + 1 && made.heapMirrors == before.heapMirrors + 1, "a sparse heap whose committed span crosses an adjacent range was not mirrored"); + Require(second[8] == 0x77, "a store to the adjacent range of a mirrored sparse heap was not written back"); + second[8] = 0x6b; + const auto again = bind(); + Require(again.buffer == first.buffer && MirrorCounters().rebuilds == made.rebuilds && MirrorCounters().sparseBinds == made.sparseBinds + 1, "a sparse heap over unchanged adjacent ranges did not reuse its mirror"); + registry(false); + sweep(); + Require(MirrorCounters().heapMirrors == before.heapMirrors && MirrorCounters().heapBytes == before.heapBytes, "the sparse heap mirror over adjacent ranges outlived its ranges"); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, reserved); + GuestArena::GuestArenaRelease_nid_postfix(block, reserved); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(guest, reserved); + munmap(mapping, reserved + page); +#endif +} + +void joinedHeapMirrorPartialImportTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t half = 1u << 20u; + constexpr std::size_t committed = 2 * half; + constexpr std::size_t reserved = 4 * half; + constexpr std::size_t page = 65536; +#ifdef _WIN32 + if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) { + std::cout << "guest arena unavailable or not write-watched: joined heap mirrors under a partial import not tested\n"; + return; + } + void* block = GuestArena::GuestArenaAllocate_nid_postfix(reserved, half); + auto* guest = static_cast(block); + GuestArena::GuestArenaCommit_nid_postfix(guest, committed, PAGE_READWRITE, page); +#else + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { + std::cout << "write watch unavailable: joined heap mirrors under a partial import not tested\n"; + return; + } + void* mapping = mmap(nullptr, reserved + half, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(mapping != MAP_FAILED, "cannot reserve the joined heap"); + auto* guest = reinterpret_cast((reinterpret_cast(mapping) + half - 1) & ~std::uintptr_t{half - 1}); + Require(mprotect(guest, committed, PROT_READ | PROT_WRITE) == 0, "cannot commit the joined heap ranges"); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(guest, reserved); + void* block = guest; +#endif + std::memset(guest, 0x3c, committed); + const auto base = reinterpret_cast(block); + const auto registry = [&](bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) { + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, guest, half, true, true, true); + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, guest + half, half, true, true, true); + } else { + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, guest); + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, guest + half); + } + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + const auto sweep = [&](const Context& with) { + GuestBufferMemory memory(with); + memory.AcquireRegistered(); + memory.Upload(true); + memory.WriteBack(); + }; + const auto bind = [&] { + GuestBufferMemory memory(context); + memory.AddWritable(base, reserved); + memory.Upload(false); + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(base, reserved, adjustment); + Require(adjustment == 0 && access.bytes(view.buffer)[view.offset + half + 32] == std::byte{0x3c}, "a heap bound across two committed ranges did not read its second range"); + memory.WriteBack(); + return view; + }; + sweep(context); + registry(true); + const auto before = MirrorCounters(); + const auto first = bind(); + const auto made = MirrorCounters(); + Require(made.heapMirrors == before.heapMirrors + 1 && made.sparseBinds == before.sparseBinds + 1, "a heap bound across two committed ranges was not mirrored over both"); + auto importing = context; + importing.hostImportAlignment = page; + SetHostImportWindow(half); + SetHostImportRangeWindow(half); + SetHostImportBudget(HostImportLiveBytes() + half); + { + GuestBufferMemory memory(importing); + memory.AcquireRegistered(); + Require(ExistingHostImportFor(importing, base, half) != nullptr && ExistingHostImportFor(importing, base + half, half) == nullptr, "the joined heap partial-import fixture did not import exactly its first range"); + Require(HeapMirrorsWithin(base, base + half).mirrors == 1, "an address build that imported one range of a joined heap mirror released the mirror"); + memory.Upload(true); + memory.WriteBack(); + } + const auto partial = MirrorCounters(); + const auto again = bind(); + Require(again.buffer == first.buffer && MirrorCounters().rebuilds == partial.rebuilds, "a joined heap mirror was rebuilt after an import of one of its ranges"); + { + GuestBufferMemory memory(importing); + memory.AcquireRegistered(); + memory.AddWritable(base + half, 32); + memory.Upload(true); + if (ExistingHostImportFor(importing, base + half, 32) != nullptr) guest[half] = 0x71; + else { + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(base + half, 32, adjustment); + access.bytes(view.buffer)[view.offset + adjustment] = std::byte{0x71}; + } + guest[half + 8] = 0x52; + memory.WriteBack(); + } + Require(guest[half] == 0x71 && guest[half + 8] == 0x52, "a joined heap address build lost GPU or genuine CPU stores"); + const auto refreshed = bind(); + Require(refreshed.buffer == first.buffer && access.bytes(refreshed.buffer)[refreshed.offset + half] == std::byte{0x71} && access.bytes(refreshed.buffer)[refreshed.offset + half + 8] == std::byte{0x52}, "the retained joined mirror did not refresh stores from an address build"); + SetHostImportBudget(HostImportLiveBytes() + committed); + GuestAllocations::GuestAllocationsEnd_nid_postfix(GuestAllocations::GuestAllocationsBegin_nid_postfix()); + sweep(importing); + Require(ExistingHostImportFor(importing, base, half) != nullptr && ExistingHostImportFor(importing, base + half, half) != nullptr, "the joined heap full-import fixture did not import both ranges"); + Require(HeapMirrorsWithin(base, base + committed).mirrors == 0, "imports covering every joined heap range left a heap mirror allocated"); + SetHostImportBudget(0); + SetHostImportWindow(0); + SetHostImportRangeWindow(std::uint64_t{64} << 20u); + ClearHostImports(context.device); + registry(false); + sweep(context); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, reserved); + GuestArena::GuestArenaRelease_nid_postfix(block, reserved); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(guest, reserved); + munmap(mapping, reserved + half); +#endif +} + void sparseHeapMirrorTests(const Context& context, const BdaTestAccess& access) { #ifdef _WIN32 if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) { @@ -3843,6 +4226,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { heapMirrorTests(context, access); partialHeapImportBudgetTests(context, access); importedHeapMirrorTests(context, access); + retiredImportWatchTests(context); importWatchTests(); importedFreshTests(context); gpuMappingTests(context); @@ -3851,8 +4235,15 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { spaceCommittedPagesTests(context, access); hostWriteWatchExclusionTests(); sparseHeapMirrorTests(context, access); + sparseJoinedHeapMirrorTests(context, access); + joinedHeapMirrorPartialImportTests(context, access); Require(AddressCopyOverflow({{0x1000, 0x3000, 0x2000, "uncommitted pages"}}, 0x2000).empty(), "copies within the limit were refused"); const auto copies = AddressCopyOverflow({{0x1000, 0x2000, 0x1000, "not mirrored"}, {0x10000, 0x30000, 0x18000, "uncommitted pages"}}, 0x2000); Require(!copies.empty() && copies.find("0x10000+0x20000 (0.1 MiB committed, uncommitted pages)") < copies.find("0x1000+0x1000"), "the copy limit does not name the largest copy first"); + AgcDriver::GuestMemory::Unwatch(0x10000, 0x20000); + AgcDriver::GuestMemory::Unwatch(0x40000, 0x10000); + Require(!AgcDriver::GuestMemory::Watched(0x10000, 0x20000), "the unregistered heap copy range is write-watched"); + const auto heapCopies = AddressCopyOverflow({{0x10000, 0x30000, 0x18000, nullptr, true, true}, {0x40000, 0x50000, 0x10000, nullptr, true, false}}, 0x2000); + Require(heapCopies.find("0x10000+0x20000 (0.1 MiB committed, import over the host import budget, heap not fully write-watched)") != std::string::npos && heapCopies.find("0x40000+0x10000 (0.1 MiB committed, heap not fully write-watched)") != std::string::npos, "the copy limit does not name a copied heap's reason"); ClearSnapshotCopies(context.device); } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp index 49cd994d6e..7479c1f4b1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp @@ -21,6 +21,7 @@ void RunHeapMirrorUnwatchTests(const AgcDriver::Graphics::Context& context, cons void RunSparseHeapMirrorUnwatchTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); void RunSparseHeapMirrorBudgetTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); void RunHeapMirrorRunUnwatchTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); +void RunAddressCopyCapTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access, bool remembered); void RunGuestAllocationTests(); void RunGuestLeaseWaitTests(); void RunUnmappedGapTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/DeferredDestruction.cpp b/core/libs/prx/libSceAgcDriver/tests/DeferredDestruction.cpp new file mode 100644 index 0000000000..7873086b80 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/DeferredDestruction.cpp @@ -0,0 +1,131 @@ +#include "prx/libSceAgcDriver/Graphics/include/DeferredDestruction.hpp" +#include +#include +#include +#include +#include +#include +#include + +using namespace AgcDriver::Graphics; + +namespace { + +int failures = 0; + +void Expect(bool condition, const std::string& what) { + if (condition) return; + std::fprintf(stderr, "FAIL: %s\n", what.c_str()); + ++failures; +} + +std::vector events; +VkResult idleResult = VK_SUCCESS; + +template +THandle Handle(std::uintptr_t value) { + return reinterpret_cast(value); +} + +std::string Name(const char* kind, const void* handle) { + return std::string(kind) + std::to_string(reinterpret_cast(handle)); +} + +VKAPI_ATTR void VKAPI_CALL mockDestroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks*) { + events.push_back(Name("buffer ", reinterpret_cast(buffer)) + Name(" on ", device)); +} + +VKAPI_ATTR void VKAPI_CALL mockFreeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks*) { + events.push_back(Name("memory ", reinterpret_cast(memory)) + Name(" on ", device)); +} + +VKAPI_ATTR VkResult VKAPI_CALL mockQueueWaitIdle(VkQueue queue) { + events.push_back(Name("idle ", queue)); + return idleResult; +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { + if (std::strcmp(name, "vkDestroyBuffer") == 0) return reinterpret_cast(&mockDestroyBuffer); + if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast(&mockFreeMemory); + if (std::strcmp(name, "vkQueueWaitIdle") == 0) return reinterpret_cast(&mockQueueWaitIdle); + return nullptr; +} + +Context ContextFor(VkDevice device, PFN_vkGetDeviceProcAddr proc) { + Context context{}; + context.device = device; + context.queue = Handle(0x900); + context.deviceProc = proc; + return context; +} + +void DeferredUntilTheQueueIsIdle() { + events.clear(); + const auto device = Handle(0x100); + const auto proc = DeferDestructionToIdle(device, mockProc); + const auto context = ContextFor(device, proc); + Expect(context.Function("vkQueueWaitIdle") == &mockQueueWaitIdle, "a function other than vkDestroyBuffer and vkFreeMemory was not passed through"); + context.Function("vkFreeMemory")(device, Handle(0x21), nullptr); + context.Function("vkDestroyBuffer")(device, Handle(0x11), nullptr); + context.Function("vkDestroyBuffer")(device, VK_NULL_HANDLE, nullptr); + Expect(events.empty() && DeferredObjects(device) == 2, "a destroy or free was not deferred, or a null handle was queued"); + const auto start = std::chrono::steady_clock::now(); + DestroyDeferredAtIdle(context, start); + Expect(events.empty() && DeferredObjects(device) == 2, "deferred objects younger than the drain age were destroyed, or the queue was waited for"); + DestroyDeferredAtIdle(context, start + std::chrono::seconds(1)); + const std::vector drained{"idle 2304", "buffer 17 on 256", "memory 33 on 256"}; + Expect(events == drained && DeferredObjects(device) == 0, "old deferred objects were not destroyed after a queue wait, buffers before memory"); + events.clear(); + for (std::uintptr_t index = 0; index < 512; ++index) context.Function("vkDestroyBuffer")(device, Handle(0x1000 + index), nullptr); + DestroyDeferredAtIdle(context, std::chrono::steady_clock::now()); + Expect(events.size() == 513 && events.front() == "idle 2304" && DeferredObjects(device) == 0, "512 young deferred objects were not destroyed after a queue wait"); + events.clear(); + idleResult = VK_ERROR_OUT_OF_HOST_MEMORY; + context.Function("vkFreeMemory")(device, Handle(0x22), nullptr); + bool threw = false; + try { + DestroyDeferredAtIdle(context, std::chrono::steady_clock::now() + std::chrono::seconds(1)); + } catch (const std::exception&) { + threw = true; + } + Expect(threw && DeferredObjects(device) == 1 && events.size() == 1, "a failed queue wait did not throw, or destroyed the deferred objects"); + idleResult = VK_SUCCESS; + events.clear(); + DestroyDeferredObjects(device); + Expect(events == std::vector{"memory 34 on 256"} && DeferredObjects(device) == 0, "the deferred objects were not destroyed when the device went, or the queue was waited for"); + events.clear(); + context.Function("vkDestroyBuffer")(device, Handle(0x12), nullptr); + Expect(events == std::vector{"buffer 18 on 256"}, "a destroy after the device went was deferred"); +} + +void OtherDevicesDestroyAtOnce() { + events.clear(); + const auto device = Handle(0x101); + const auto proc = DeferDestructionToIdle(device, mockProc); + const auto other = Handle(0x102); + reinterpret_cast(proc(other, "vkFreeMemory"))(other, Handle(0x23), nullptr); + Expect(events == std::vector{"memory 35 on 258"} && DeferredObjects(other) == 0, "a free on a device without deferred destruction was deferred"); + bool threw = false; + try { + DeferDestructionToIdle(device, mockProc); + } catch (const std::exception&) { + threw = true; + } + Expect(threw, "deferred destruction was set up twice for one device"); + DestroyDeferredObjects(device); +} + +} + +int main() { + try { + DeferredUntilTheQueueIsIdle(); + OtherDevicesDestroyAtOnce(); + } catch (const std::exception& error) { + std::fprintf(stderr, "FAIL: %s\n", error.what()); + return 1; + } + if (failures != 0) return 1; + std::puts("deferred destruction tests passed"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp b/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp index cc952f5ea9..5dd5cfc4ba 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp @@ -110,7 +110,8 @@ std::vector Recompiled(std::span code) { request.target.maxWorkgroupSharedMemoryBytes = 49152; request.layout = {0, 0, 0, 128}; request.useCache = false; - const auto& words = Recompile(request).spirv.Words(); + const auto result = Recompile(request); + const auto& words = result.spirv.Words(); return {words.begin(), words.end()}; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp index 9e89fdd61c..849703ad95 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentMode.hpp" +#include "prx/libSceAgcDriver/Execution/include/SwapchainImages.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" @@ -406,6 +407,19 @@ void testReset(bool compute) { void testPresentMode() { using AgcDriver::SelectPresentMode; + VkSurfaceCapabilitiesKHR surface{}; + surface.minImageCount = 2; + surface.maxImageCount = 8; + check(AgcDriver::SwapchainImageCount(surface) == 3, "the swapchain has no image beyond the surface minimum"); + surface.minImageCount = 3; + check(AgcDriver::SwapchainImageCount(surface) == 3, "a three-image surface minimum gained an image"); + surface.minImageCount = 5; + check(AgcDriver::SwapchainImageCount(surface) == 5, "the swapchain does not follow a larger surface minimum"); + surface.minImageCount = 2; + surface.maxImageCount = 2; + check(AgcDriver::SwapchainImageCount(surface) == 2, "the swapchain exceeds the surface maximum"); + surface.maxImageCount = 0; + check(AgcDriver::SwapchainImageCount(surface) == 3, "an unlimited surface maximum capped the swapchain"); const std::array wayland{VK_PRESENT_MODE_FIFO_KHR, VK_PRESENT_MODE_IMMEDIATE_KHR}; const std::array all{VK_PRESENT_MODE_IMMEDIATE_KHR, VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_FIFO_KHR}; check(SelectPresentMode(nullptr, wayland) == VK_PRESENT_MODE_FIFO_KHR, "the default present mode is not FIFO"); @@ -421,6 +435,11 @@ void testPresentMode() { check(expectFailure([&] { SelectPresentMode("", all); }).find("must be fifo, fifo_latest_ready, mailbox or immediate") != std::string::npos, "an empty present mode name was accepted"); const std::array none{}; check(expectFailure([&] { SelectPresentMode(nullptr, none); }).find("does not support") != std::string::npos, "FIFO was selected on a surface without it"); + using AgcDriver::SelectPresentQueue; + check(SelectPresentQueue(16, 2) == 2u, "the presentation queue does not follow the main and settle queues"); + check(SelectPresentQueue(16, 1) == 1u, "the presentation queue does not follow the main queue alone"); + check(!SelectPresentQueue(2, 2).has_value(), "a presentation queue was taken from a family without a free queue"); + check(!SelectPresentQueue(1, 1).has_value(), "a presentation queue was taken from a family with one queue"); } int main(int argc, char** argv) { diff --git a/core/libs/prx/libSceAgcDriver/tests/FrameReplay.cpp b/core/libs/prx/libSceAgcDriver/tests/FrameReplay.cpp index dc652cddfe..23e6c21513 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FrameReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FrameReplay.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #ifdef _WIN32 #include @@ -100,14 +101,18 @@ std::vector pattern(std::uint8_t seed) { return bytes; } -void copyFrame(std::uint32_t* words, const Layout& layout, std::byte* target) { - CommandBuffer buffer{words, words + CommandBytes / sizeof(std::uint32_t), words, words + CommandBytes / sizeof(std::uint32_t), nullptr, nullptr, 0}; - sceAgcDcbDmaData(&buffer, 0, DmaMemory, 0, at(target), DmaMemory, 0, at(layout.source), DataBytes, 0, 1, 0); - sceAgcDcbWriteData(&buffer, GraphicsMemory, 0, at(layout.label), &Done, 1, 0, 1); +void copy(std::uint32_t* words, std::size_t commandBytes, const std::byte* source, std::byte* target, std::uint32_t* label) { + CommandBuffer buffer{words, words + commandBytes / sizeof(std::uint32_t), words, words + commandBytes / sizeof(std::uint32_t), nullptr, nullptr, 0}; + sceAgcDcbDmaData(&buffer, 0, DmaMemory, 0, at(target), DmaMemory, 0, at(source), DataBytes, 0, 1, 0); + sceAgcDcbWriteData(&buffer, GraphicsMemory, 0, at(label), &Done, 1, 0, 1); Packet packet{words, static_cast(buffer.cursor_up - words), 0, {}}; check(sceAgcDriverSubmitDcb(&packet) == 0, "submission failed"); } +void copyFrame(std::uint32_t* words, const Layout& layout, std::byte* target) { + copy(words, CommandBytes, layout.source, target, layout.label); +} + void flip() { AgcDriverSubmitFlip_nid_postfix(AgcDriver::FlipInfo{Handle, -1, 1, 0}); } @@ -187,15 +192,75 @@ void testCaptureAndReplay(const std::filesystem::path& tool) { check(readFile(secondDump) == second, "the replay did not reproduce the second copy"); } +void testConcurrentCopies(const std::filesystem::path& tool) { + constexpr std::size_t Copies = 64; + constexpr std::size_t Frames = 8; + constexpr std::size_t CopyCommandBytes = 1024; + const char* directory = std::getenv("APS5_FRAME_CAPTURE"); + check(directory != nullptr && *directory != '\0', "APS5_FRAME_CAPTURE is not set"); + const std::filesystem::path capture(directory); + const auto layout = allocate(); + const auto other = allocate(); + auto* const otherSource = other.base + 128 * 1024; + auto* const targets = otherSource + DataBytes; + auto* const otherLabel = reinterpret_cast(other.base + 512 * 1024); + const auto copied = pattern(57); + std::memcpy(otherSource, copied.data(), copied.size()); + auto output = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(Handle, output); + + flip(); + std::thread copier([&] { + for (std::size_t index = 0; index < Copies; ++index) { + copy(reinterpret_cast(other.base + index * CopyCommandBytes), CopyCommandBytes, otherSource, targets + index * DataBytes, otherLabel); + } + }); + for (std::size_t frame = 0; frame + 1 < Frames; ++frame) { + const auto bytes = pattern(static_cast(frame)); + std::memcpy(layout.source, bytes.data(), bytes.size()); + copyFrame(layout.firstCommands, layout, layout.firstTarget); + flip(); + } + copier.join(); + flip(); + AgcDriverWaitIdle_nid_postfix(); + + const FrameCaptureReader reader(capture); + check(!reader.Truncated(), "the capture is truncated"); + std::uint64_t base = 0; + std::size_t submits = 0, memory = 0; + for (const auto& call : reader.Calls()) { + if (call.kind == FrameCallKind::FrameStart) base = call.completed; + if (call.kind == FrameCallKind::Memory) { + ++memory; + check(call.completed >= base + submits, "a memory delta was read with " + std::to_string(base + submits - call.completed) + " of the " + std::to_string(submits) + " submissions recorded before it unfinished"); + } + submits += call.kind == FrameCallKind::Submit; + } + check(submits == Copies + Frames - 1, "the capture holds " + std::to_string(submits) + " submits, not " + std::to_string(Copies + Frames - 1)); + check(memory != 0, "the capture holds no memory deltas"); + + const auto dump = capture / "targets.bin"; + const auto command = "\"\"" + tool.string() + "\" \"" + capture.string() + "\" --strict --dump " + std::to_string(at(targets)) + ":" + std::to_string(Copies * DataBytes) + ":\"" + dump.string() + "\"\""; + const auto code = std::system(command.c_str()); + check(code == 0, "agc_frame_replay exited with " + std::to_string(code)); + const auto replayed = readFile(dump); + check(replayed.size() == Copies * DataBytes, "the replay dumped " + std::to_string(replayed.size()) + " bytes of the copy targets"); + for (std::size_t index = 0; index < Copies; ++index) { + check(std::memcmp(replayed.data() + index * DataBytes, copied.data(), DataBytes) == 0, "the replay did not reproduce copy " + std::to_string(index) + " of the second thread"); + } +} + } int main(int argc, char** argv) { - if (argc != 2) { - std::fprintf(stderr, "usage: agc_driver_frame_replay_tests \n"); + if (argc != 2 && !(argc == 3 && std::string(argv[2]) == "concurrent")) { + std::fprintf(stderr, "usage: agc_driver_frame_replay_tests [concurrent]\n"); return 2; } try { - testCaptureAndReplay(argv[1]); + if (argc == 3) testConcurrentCopies(argv[1]); + else testCaptureAndReplay(argv[1]); } catch (const std::exception& error) { std::fprintf(stderr, "FAILED: %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 95080d73f9..e1896d291f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3134,6 +3134,13 @@ void metadataPassTests() { auto mipmapped = queue; mipmapped.context[0x3b0] |= 1u << 28u; expectFailure([&] { DecodeColorMetadataPass(mipmapped); }, "mipmapped DCC"); + const auto chain = AgcDriver::Graphics::DecodeColorBuffer(mipmapped.context, 0); + const auto chainSurface = AgcDriver::Graphics::SurfaceForTarget(chain); + Require(chain.dccAddress == 0 && chainSurface.mipCount == 2 && chainSurface.dccAddress == keysAddress, "the surface of a mipmapped DCC color target lost its keys"); + auto mipmappedArray = mipmapped; + mipmappedArray.context[0x3b8] |= 1u; + mipmappedArray.context[0x31b] = 1u << 13u; + Require(AgcDriver::Graphics::SurfaceForTarget(AgcDriver::Graphics::DecodeColorBuffer(mipmappedArray.context, 0)).dccAddress == 0, "a mipmapped DCC array color target took keys whose layers are not modeled"); const AgcDriver::Graphics::Context context{}; const auto texels = [&](std::uint32_t value) { for (std::size_t offset = 0; offset < colorMemory.size(); offset += 4) { @@ -3966,6 +3973,10 @@ void multisampleTests() { for (const auto offset : {0x31cu, 0x32bu}) packed.context[offset] = (packed.context[offset] & ~0x41f7cu) | packedInfo; expectFailure([&] { DecodeColorMetadataPass(packed); }, std::string("CB resolves of ") + name + " color targets"); } + auto uintSource = rasterizedResolve; + for (const auto offset : {0x31cu, 0x32bu}) uintSource.context[offset] = (uintSource.context[offset] & ~0x700u) | (4u << 8u); + const auto uintPass = DecodeColorMetadataPass(uintSource); + Require(uintPass.has_value() && uintPass->mode == ColorMetadataPass::Mode::Resolve && uintPass->source.has_value() && uintPass->source->packing == ShaderRecompiler::ColorExportPacking::Uint && uintPass->targets.size() == 1, "a CB resolve of an unsigned integer target was refused"); queue = rasterizedResolve; queue.context[0x31c] |= 0x10000000u; queue.context[0x325] = 0; @@ -4667,6 +4678,72 @@ void DepthRemapTests() { Require(floating.depthRemap.mode == 1 && floating.viewport.minDepth == 0 && floating.viewport.maxDepth == 1 && floating.depthClamp, "a written float depth target refused clipped clamp bounds"); } +void DepthBoundsEmulationTests() { + auto queue = makeState(); + queue.context[0x204] = 0; + queue.context[0x113] = std::bit_cast(0.5f); + queue.context[0x114] = std::bit_cast(0.5f); + auto base = AgcDriver::Graphics::DecodeState(queue); + base.depth = AgcDriver::Graphics::DepthTarget{0x10000, 0, {64, 64}, VK_FORMAT_D32_SFLOAT_S8_UINT, 0.0f, 0}; + base.depthTest = base.depthWrite = base.stencilTest = false; + base.depthBoundsTest = true; + base.depthBias = true; + base.depthBiasConstant = 4.0f; + const ShaderRecompiler::ShaderPixelStageInfo pixel{}; + const AgcDriver::Graphics::Context emulating{}; + const auto emulated = [&](float minBound, float maxBound) { + auto state = base; + state.minDepthBounds = minBound; + state.maxDepthBounds = maxBound; + AgcDriver::Graphics::ApplyDepthBoundsEmulation(emulating, state, pixel); + Require(!state.depthBoundsTest && state.depthTest && !state.depthWrite && !state.depthBias && state.viewport.x == base.viewport.x && state.viewport.width == base.viewport.width && state.viewport.height == base.viewport.height, "an emulated depth bounds test did not become a read-only depth test over the same coverage"); + return state; + }; + const auto upper = emulated(0.0f, 0.000125f); + Require(upper.depthCompare == VK_COMPARE_OP_GREATER_OR_EQUAL && upper.viewport.minDepth == 0.000125f && upper.viewport.maxDepth == 0.000125f, "an upper depth bound was not tested as the bound at or above the stored depth"); + const auto lower = emulated(0.000125f, 1.0f); + Require(lower.depthCompare == VK_COMPARE_OP_LESS_OR_EQUAL && lower.viewport.minDepth == 0.000125f && lower.viewport.maxDepth == 0.000125f, "a lower depth bound was not tested as the bound at or below the stored depth"); + const auto whole = emulated(0.0f, 1.0f); + Require(whole.depthCompare == VK_COMPARE_OP_GREATER_OR_EQUAL && whole.viewport.minDepth == 1.0f && whole.viewport.maxDepth == 1.0f, "bounds covering [0, 1] were not tested against the upper bound"); + AgcDriver::Graphics::Context native{}; + native.depthBounds = true; + auto kept = base; + AgcDriver::Graphics::ApplyDepthBoundsEmulation(native, kept, pixel); + Require(kept.depthBoundsTest && !kept.depthTest && kept.depthBias && kept.viewport.minDepth == base.viewport.minDepth, "a device with the depth bounds test had it emulated"); + auto absent = base; + absent.depth.reset(); + AgcDriver::Graphics::ApplyDepthBoundsEmulation(emulating, absent, pixel); + Require(absent.depthBoundsTest && !absent.depthTest, "a draw without a depth target had its depth bounds emulated"); + const auto refuses = [&](const char* expected, auto change) { + auto state = base; + state.minDepthBounds = 0.0f; + state.maxDepthBounds = 0.5f; + auto context = emulating; + auto program = pixel; + change(state, context, program); + expectFailure([&] { AgcDriver::Graphics::ApplyDepthBoundsEmulation(context, state, program); }, expected); + }; + refuses("both bounds inside", [](auto& state, auto&, auto&) { state.minDepthBounds = 0.25f; }); + refuses("with depth testing", [](auto& state, auto&, auto&) { state.depthTest = true; }); + refuses("reads or exports depth", [](auto&, auto&, auto& program) { program.posZ = true; }); + refuses("reads or exports depth", [](auto&, auto&, auto& program) { program.depthExportEnable = true; }); + refuses("normalized depth target", [](auto& state, auto&, auto&) { state.depth->format = VK_FORMAT_D16_UNORM; }); + refuses("stays in [0, 1]", [](auto&, auto& context, auto&) { context.depthRangeUnrestricted = true; }); + refuses("depth bounds outside [0, 1]", [](auto& state, auto&, auto&) { state.maxDepthBounds = 1.5f; }); + refuses("writes on a failed test", [](auto& state, auto&, auto&) { + state.stencilTest = true; + state.stencilFront.writeMask = 0xffu; + state.stencilFront.failOp = VK_STENCIL_OP_REPLACE; + }); + auto stencil = base; + stencil.stencilTest = true; + stencil.stencilFront.writeMask = stencil.stencilBack.writeMask = 0xffu; + stencil.stencilFront.passOp = stencil.stencilBack.passOp = VK_STENCIL_OP_REPLACE; + stencil.maxDepthBounds = 0.5f; + AgcDriver::Graphics::ApplyDepthBoundsEmulation(emulating, stencil, pixel); + Require(stencil.depthTest && stencil.stencilTest, "a stencil test that writes only on a pass refused the depth bounds emulation"); +} + void UnusedViewportDepthTests() { auto queue = makeState(); queue.context[0x204] = 0; @@ -7092,6 +7169,27 @@ void storageTextureBudgetTests() { memory.memoryHeaps[0].flags = 0; memory.memoryHeaps[2].flags = 0; Require(StorageTextureBudget(memory, &reported, 0, 0) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of storage images"); + using AgcDriver::Graphics::StorageTextureCeiling; + memory.memoryHeaps[0] = {256 * MiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + memory.memoryHeaps[2] = {16 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + reported = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + Require(StorageTextureCeiling(memory, nullptr) == 4 * GiB, "without VK_EXT_memory_budget the storage image cache ceiling is not a quarter of the 16 GiB heap"); + reported.heapBudget[2] = 6 * GiB; + Require(StorageTextureCeiling(memory, &reported) == 3 * GiB, "the storage image cache ceiling on a 6 GiB budget is not half of the budget, the cap the cache may reach"); + reported.heapBudget[2] = 40 * GiB; + Require(StorageTextureCeiling(memory, &reported) == 8 * GiB, "the storage image cache ceiling on a 40 GiB budget is not half of the 16 GiB heap"); + reported.heapBudget[2] = 768 * MiB; + Require(StorageTextureCeiling(memory, &reported) == 512 * MiB, "the storage image cache ceiling on a 768 MiB budget does not keep the 512 MiB floor"); + for (std::uint64_t budget = 1 * GiB; budget <= 24 * GiB; budget += 512 * MiB) { + reported.heapBudget[2] = budget; + const auto ceiling = StorageTextureCeiling(memory, &reported); + for (std::uint64_t used = 0; used <= budget; used += 256 * MiB) { + reported.heapUsage[2] = used; + Require(StorageTextureBudget(memory, &reported, 256 * MiB, 512 * MiB) <= ceiling, "the storage image cache budget exceeds the ceiling idle device buffers are reclaimed up to"); + } + reported.heapUsage[2] = 0; + Require(StorageTextureBudget(memory, &reported, 0, 0) == ceiling, "an idle heap does not give the storage image cache its full ceiling"); + } } void sampledBudgetReportTests() { @@ -7138,6 +7236,41 @@ void misalignedShaderDataTests() { Require(mock.live == 0, "misaligned shader data resources leaked Vulkan objects"); } +void imageTableTemplateRefreshTests() { + namespace Abi = ShaderRecompiler::ImageTableAbi; + mock = MockVulkan{}; + auto context = mockContext(); + context.imageTableCapacity = 2; + context.limits.maxDescriptorSetSampledImages = 2; + context.limits.maxPerStageDescriptorSampledImages = 2; + const auto commands = reinterpret_cast(std::uintptr_t{1}); + { + ShaderRecompiler::RecompileResult compute; + auto map = makeBinding(Role::ImageTableMap, static_cast(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap), 1, std::vector(Abi::HeaderWords)); + map.guestDescriptor[Abi::HeaderVersion] = Abi::Version; + map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords; + map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords; + compute.bindings.push_back(map); + auto images = makeBinding(Role::ImageTable, ShaderRecompiler::RuntimeAbi::ImageTableBinding, 0, {}); + images.kind = Kind::SampledImage; + compute.bindings.push_back(images); + compute.bindings.push_back(makeBinding(Role::ShaderData, 1, 1, ShaderDataWords({0x11, 0x22}))); + compute.shaderDataDwords = ShaderRecompiler::RuntimeAbi::ShaderDataDwords; + auto live = compute; + live.bindings[2].guestDescriptor = ShaderDataWords({0x33, 0x44}); + const AgcDriver::Graphics::CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &compute, 0}; + const AgcDriver::Graphics::CompiledShader liveShader{ShaderRecompiler::ShaderStage::Compute, &live, 0}; + AgcDriver::Graphics::ShaderResources resources(context, shader); + const auto data = findWrite(1).buffers.at(0).buffer; + Require(!resources.DataWordsDiffer(shader), "a template's own words differ from the shader that built it"); + Require(resources.DataWordsDiffer(liveShader), "a template with an image table missed the dispatch's new words"); + Require(resources.RefreshData(commands, liveShader), "a refresh of a template with an image table recorded nothing"); + Require(sameBytes(bufferBytes(data), live.bindings[2].guestDescriptor.data(), 3u * sizeof(std::uint32_t)), "a refresh of a template with an image table did not write the dispatch's words"); + Require(!resources.DataWordsDiffer(liveShader), "the refreshed template's words are not the dispatch's"); + } + Require(mock.live == 0, "image table template refresh resources leaked Vulkan objects"); +} + struct ModuleShape { bool fragment = false; std::optional fragmentMode; @@ -8076,6 +8209,38 @@ void pipelineLibraryClearWaitsForBuildTests() { Require(mock.live == 0, "clearing during a library build leaked Vulkan objects"); } +void tessellationPipelineLibraryCacheTests() { + using namespace AgcDriver::Graphics; + for (const auto shared : {true, false}) { + mock = MockVulkan{}; + auto context = mockContext(); + context.graphicsPipelineLibrary = true; + context.tessellationShader = true; + context.tessellationPipelineCache = shared; + context.limits.maxTessellationPatchSize = 32; + context.limits.maxPushConstantsSize = 256; + context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT; + State state{}; + state.stages.path = ShaderPath::Tessellation; + state.stages.tessellation = ShaderRecompiler::TessellationConfiguration{3, 3, 2, 1, 4, 128, 16, false}; + ShaderRecompiler::RecompileResult vertex, control, evaluation, fragment; + vertex.spirv = makeModule({}); + control.spirv = makeModule({}); + evaluation.spirv = makeModule({}); + fragment.spirv = makeModule({.fragment = true}); + vertex.variantId = 201; + control.variantId = 202; + evaluation.variantId = 203; + fragment.variantId = 204; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::TessellationControl, &control, 0}, {ShaderRecompiler::ShaderStage::TessellationEvaluation, &evaluation, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}}; + std::array mainKey{31}; + const PipelineSetLayouts resources{makeHandle(), VK_NULL_HANDLE, false, mainKey, {}}; + Require(Pipeline::Prebuild(context, state, VertexInputLayout{}, resources, shaders) == shared, shared ? "tessellation pipelines skipped graphics pipeline libraries with a shared pipeline cache" : "tessellation pipelines without the pipeline cache were built from graphics pipeline libraries"); + ClearPipelineLibraries(context.device); + Require(mock.live == 0, "tessellation pipeline library tests leaked Vulkan objects"); + } +} + void framebufferOwnerTests() { using namespace AgcDriver::Graphics; mock = MockVulkan{}; @@ -9074,7 +9239,7 @@ void stencilRefExportTests() { Require(AgcDriver::Graphics::DrawRejection(unused, false).empty(), "the precheck rejected STENCIL_OP_VAL_EXPORT_ENABLE without a stencil test in DB_SHADER_CONTROL " + std::to_string(shaderControl)); Require(AgcDriver::Graphics::DecodePixelStageInfo(unused.context, IdentityExports).stencilRefExportEnable == ((shaderControl & 2u) != 0), "STENCIL_OP_VAL_EXPORT_ENABLE without a stencil test exported the stencil reference"); const bool replaces = (shaderControl & 6u) == 4u; - for (const auto [ops, writeMask, compare, accepted] : {std::tuple{0x000u, 0xffu, 7u, true}, std::tuple{0x737u, 0xffu, 7u, true}, std::tuple{0x110u, 0xffu, 7u, true}, std::tuple{0x050u, 0x00u, 7u, true}, std::tuple{0x040u, 0xffu, 7u, replaces}, std::tuple{0x40040u, 0xffu, 7u, replaces}, std::tuple{0x404u, 0x0fu, 0u, replaces}, std::tuple{0x040u, 0xffu, 2u, false}, std::tuple{0x340u, 0xffu, 7u, false}, std::tuple{0x042u, 0xffu, 7u, false}, std::tuple{0x050u, 0xffu, 7u, false}, std::tuple{0x600u, 0xffu, 7u, false}, std::tuple{0x008u, 0xffu, 7u, false}, std::tuple{0x900u, 0xffu, 7u, false}, std::tuple{0x5000u, 0xffu, 7u, false}}) { + for (const auto [ops, writeMask, compare, accepted] : {std::tuple{0x000u, 0xffu, 7u, true}, std::tuple{0x737u, 0xffu, 7u, true}, std::tuple{0x110u, 0xffu, 7u, true}, std::tuple{0x050u, 0x00u, 7u, true}, std::tuple{0x040u, 0xffu, 7u, replaces}, std::tuple{0x40040u, 0xffu, 7u, replaces}, std::tuple{0x404u, 0x0fu, 0u, replaces}, std::tuple{0x040u, 0xffu, 2u, false}, std::tuple{0x340u, 0xffu, 7u, false}, std::tuple{0x042u, 0xffu, 7u, false}, std::tuple{0x050u, 0xffu, 7u, false}, std::tuple{0x600u, 0xffu, 7u, false}, std::tuple{0x008u, 0xffu, 7u, false}, std::tuple{0x900u, 0xffu, 7u, false}, std::tuple{0x5000u, 0xffu, 7u, false}, std::tuple{0x0c0u, 0xffu, 7u, false}, std::tuple{0x00bu, 0xffu, 7u, false}}) { auto stenciled = stencilRefExportQueue(shaderControl, zFormat); for (const auto [offset, word] : {std::pair{0x010u, 0x80000180u}, std::pair{0x011u, 0x20000181u}, std::pair{0x012u, 0x00001000u}, std::pair{0x013u, 0x00002000u}, std::pair{0x015u, 0x00002000u}, std::pair{0x007u, 0x003f003fu}, std::pair{0x000u, 0u}, std::pair{0x002u, 0u}, std::pair{0x00au, 0u}, std::pair{0x00bu, 0x3f800000u}, std::pair{0x200u, 0x007007b3u}}) stenciled.context[offset] = word; stenciled.context[0x10b] = ops; @@ -9384,6 +9549,99 @@ void stencilReferenceUnusedTests() { std::cout << "Unused stencil reference sample equivalence passed: " << accepted << " accepted states, " << countermodels << " dependent countermodels\n"; } +void stencilLogicOperationTests() { + const auto queueFor = [](std::uint32_t ops, std::uint32_t front, std::uint32_t back, std::uint32_t depthControl) { + auto queue = makeState(); + for (const auto [offset, word] : {std::pair{0x000u, 0u}, std::pair{0x002u, 0u}, std::pair{0x007u, (1u << 16u) | 3u}, std::pair{0x00au, 0u}, std::pair{0x00bu, 0u}, std::pair{0x010u, 0x22900983u}, std::pair{0x011u, 0x20000181u}, std::pair{0x012u, 0x100u}, std::pair{0x014u, 0x100u}, std::pair{0x013u, 0x200u}, std::pair{0x015u, 0x200u}, std::pair{0x005u, 0x300u}, std::pair{0x01eu, 0x1u}}) queue.context[offset] = word; + queue.context[0x10b] = ops; + queue.context[0x10c] = front; + queue.context[0x10d] = back; + queue.context[0x200] = depthControl; + return queue; + }; + const auto refMaskOf = [](std::uint32_t test, std::uint32_t writeMask, std::uint32_t value) { return test | 0xff00u | (writeMask << 16u) | (value << 24u); }; + const auto apply = [](std::uint32_t op, std::uint32_t stencil, std::uint32_t value) { + switch (op) { + case 1: return 0u; + case 10: return stencil & value; + case 11: return stencil | value; + case 12: return stencil ^ value; + case 15: return ~(stencil ^ value); + default: return stencil; + } + }; + const auto matches = [&](const AgcDriver::Graphics::State& state, const VkStencilOpState& face, std::uint32_t ops, std::uint32_t compare, std::uint32_t refMask, const std::string& name) { + const auto test = refMask & 0xffu; + const auto compareMask = (refMask >> 8u) & 0xffu; + const auto writeMask = (refMask >> 16u) & 0xffu; + const auto value = refMask >> 24u; + for (std::uint32_t stencil = 0; stencil < 256u; ++stencil) { + for (const bool depthPass : {false, true}) { + const bool stencilPass = stencilComparison(static_cast(compare), test & compareMask, stencil & compareMask); + const auto op = (ops >> (!stencilPass ? 0u : !depthPass ? 8u : 4u)) & 0xfu; + const auto expected = ((stencil & ~writeMask) | (apply(op, stencil, value) & writeMask)) & 0xffu; + const auto [actual, passed] = stencilReferenceOutcome(state, face, face.reference, stencil, depthPass, true); + Require(actual == expected && passed == (stencilPass && depthPass), name + ": stencil " + std::to_string(stencil) + (depthPass ? " passing" : " failing") + " the depth test became " + std::to_string(actual) + ", expected " + std::to_string(expected)); + } + } + }; + std::size_t accepted = 0; + for (const auto op : {10u, 11u, 12u, 15u}) { + for (const auto placement : {0x010u, 0x001u, 0x100u, 0x111u}) { + for (const auto compare : {7u, 0u, 2u, 3u}) { + for (const auto [test, writeMask, value] : {std::tuple{0x00u, 0xffu, 0x5au}, std::tuple{0xa5u, 0xffu, 0x21u}, std::tuple{0xa5u, 0x3cu, 0xffu}, std::tuple{0xffu, 0xf0u, 0x0fu}, std::tuple{0x5au, 0x81u, 0x00u}}) { + const auto ops = op * placement; + const auto refMask = refMaskOf(test, writeMask, value); + const auto queue = queueFor(ops, refMask, refMask, 0x13u | (compare << 8u)); + const auto name = "stencil operation " + std::to_string(op) + " in DB_STENCIL_CONTROL " + std::to_string(ops) + " with compare " + std::to_string(compare) + " and DB_STENCILREFMASK " + std::to_string(refMask); + const auto bits = (op == 11u || op == 12u ? value : ~value) & writeMask; + if (op == 11u && compare != 0u && compare != 7u && (~test & bits) != 0u) { + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "stencil OR of"); + continue; + } + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilTest && state.depthTest && (state.stencilFront.writeMask & 0xffu) == (bits != 0u ? bits : writeMask), "the write mask is not narrowed to the bits changed by " + name); + matches(state, state.stencilFront, ops, compare, refMask, name); + ++accepted; + } + } + } + } + for (const auto [ops, refMask, name] : {std::tuple{0x0c1u, refMaskOf(0x80u, 0xffu, 0xffu), "XOR of every written bit beside ZERO"}, std::tuple{0x0cbu, refMaskOf(0x8fu, 0xffu, 0x0fu), "OR and XOR of the same bits"}, std::tuple{0x0fau, refMaskOf(0x80u, 0xffu, 0xf0u), "AND and XNOR of the same bits"}, std::tuple{0xb0bu, refMaskOf(0x30u, 0x3cu, 0xf0u), "OR on stencil fail and depth fail"}}) { + const auto queue = queueFor(ops, refMask, refMask, 0x13u | (3u << 8u)); + const auto state = AgcDriver::Graphics::DecodeState(queue); + matches(state, state.stencilFront, ops, 3u, refMask, name); + ++accepted; + } + { + const auto front = refMaskOf(0x11u, 0xffu, 0x0fu); + const auto back = refMaskOf(0x22u, 0x3cu, 0x30u); + const auto queue = queueFor(0x0c0u | (0x0a0u << 12u), front, back, 0x93u | (7u << 8u) | (5u << 20u)); + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilFront.passOp == VK_STENCIL_OP_INVERT && state.stencilFront.writeMask == 0x0fu && state.stencilBack.passOp == VK_STENCIL_OP_ZERO && state.stencilBack.writeMask == 0x0cu, "front XOR and back AND did not keep their own write masks"); + matches(state, state.stencilFront, 0x0c0u, 7u, front, "front XOR beside a back AND"); + matches(state, state.stencilBack, 0x0a0u, 5u, back, "back AND beside a front XOR"); + accepted += 2; + } + for (const auto [ops, refMask, reason] : {std::tuple{0x0d0u, refMaskOf(0u, 0xffu, 0x0fu), "stencil operation 13 is unsupported"}, std::tuple{0x0e0u, refMaskOf(0u, 0xffu, 0x0fu), "stencil operation 14 is unsupported"}, std::tuple{0x0cau, refMaskOf(0u, 0xffu, 0x0fu), "stencil logic operations on write masks"}, std::tuple{0x0fcu, refMaskOf(0u, 0xffu, 0x0fu), "stencil logic operations on write masks"},std::tuple{0x0c1u, refMaskOf(0u, 0xffu, 0x0fu), "combine a logic operation"}, std::tuple{0x5c0u, refMaskOf(0u, 0xffu, 0x01u), "combine a logic operation"}}) { + const auto queue = queueFor(ops, refMask, refMask, 0x13u | (7u << 8u)); + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, reason); + } + for (const auto ops : {0x0c0u, 0x0b0u}) { + auto exported = stencilRefExportQueue(0x12u, 9u); + for (const auto [offset, word] : {std::pair{0x010u, 0x80000180u}, std::pair{0x011u, 0x20000181u}, std::pair{0x012u, 0x00001000u}, std::pair{0x013u, 0x00002000u}, std::pair{0x015u, 0x00002000u}, std::pair{0x007u, 0x003f003fu}, std::pair{0x000u, 0u}, std::pair{0x002u, 0u}, std::pair{0x00au, 0u}, std::pair{0x00bu, 0x3f800000u}, std::pair{0x200u, 0x007007b3u}}) exported.context[offset] = word; + exported.context[0x10b] = ops; + exported.context[0x10c] = exported.context[0x10d] = refMaskOf(0u, 0xffu, 0x0fu); + if (ops == 0x0c0u) { + const auto state = AgcDriver::Graphics::DecodeState(exported); + Require(state.stencilFront.passOp == VK_STENCIL_OP_INVERT && state.stencilFront.writeMask == 0x0fu, "a stencil test value export changed a stencil XOR"); + } else { + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(exported)); }, "stencil OR is unsupported with a pixel shader that exports the stencil test value"); + } + } + std::cout << "Stencil logic operations matched AND, OR, XOR and XNOR under the write mask for " << accepted << " register states\n"; +} + std::set bufferViews(std::span words) { std::set views; for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { @@ -9898,6 +10156,34 @@ ShaderRecompiler::RecompileRequest packedExportRequest(const AgcDriver::QueueSta return request; } +void uintTargetTests() { + using ShaderRecompiler::ColorExportPacking; + auto queue = makeState(); + queue.context[0x31c] = (queue.context.at(0x31c) & ~0x1f7cu) | (4u << 2u) | (4u << 8u); + queue.context[0x1e0] = 1u << 30u; + for (std::uint32_t i = 0; i < 4; ++i) queue.context[0x105 + i] = 0; + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.color.format == VK_FORMAT_R32_UINT && state.color.elementBytes == 4 && state.color.bytes == colorMemory.size() && state.color.packing == ColorExportPacking::Uint, "a 32-bit UINT target did not get the unsigned integer export packing"); + const auto packings = AgcDriver::Graphics::ExportPackings(state); + Require(packings[0] == ColorExportPacking::Uint && std::all_of(packings.begin() + 1, packings.end(), [](ColorExportPacking packing) { return packing == ColorExportPacking::None; }), "export 0 did not carry the unsigned integer packing"); + Require(!state.blends.at(0).blendEnable && state.blends.at(0).colorWriteMask != 0, "blending into a UINT target was not dropped"); + auto bytes = queue; + bytes.context[0x31c] = (bytes.context.at(0x31c) & ~0x1f7cu) | (1u << 2u) | (4u << 8u); + const auto eight = AgcDriver::Graphics::DecodeState(bytes); + Require(eight.color.format == VK_FORMAT_R8_UINT && eight.color.packing == ColorExportPacking::Uint && !eight.blends.at(0).blendEnable, "an 8-bit UINT target did not get the unsigned integer export packing"); + auto words = queue; + words.context[0x1c5] = 7u; + words.context[0x1e0] = 0; + const auto uint16 = AgcDriver::Graphics::DecodeState(words); + 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"); + 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"); +} + void unorm10_11_11TargetTests() { using ShaderRecompiler::ColorExportPacking; auto queue = makeState(); @@ -10375,7 +10661,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(4); + invalid.context.pixel->targetExportPacking[0] = static_cast(5); expectFailure([&] { static_cast(ShaderRecompiler::Recompile(invalid)); }, "invalid prepared fragment export packing"); } @@ -11378,6 +11664,117 @@ void pipelineStoreLazyBufferPoolTests() { Require(mock.live == 0, "lazy buffer pool pipeline tests leaked Vulkan objects"); } +// The pipeline store's bound without a GPU: 1536 configurations cycled through the default 1024 +// entries keep coming back after their eviction, so the bound doubles and a later pass finds every +// pipeline resident; a bound set with APS5_PIPELINE_STORE_ENTRIES never moves. +void pipelineStoreGrowthTests(bool fixed) { + using namespace AgcDriver::Graphics; + mock = MockVulkan{}; + auto context = mockContext(); + context.device = makeHandle(); + context.graphicsPipelineLibrary = true; + context.pipelineLinkTimeOptimization = false; + context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT; + ShaderRecompiler::RecompileResult vertex, fragment; + vertex.spirv = makeModule({}); + fragment.spirv = makeModule({.fragment = true}); + vertex.variantId = 301; + fragment.variantId = 302; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}}; + std::array mainKey{41}, fragmentKey{42}; + const PipelineSetLayouts resources{makeHandle(), makeHandle(), true, mainKey, fragmentKey}; + constexpr std::uint32_t configurations = 1536; + const auto pass = [&](std::uint32_t count) { + std::uint32_t built = 0; + for (std::uint32_t configuration = 0; configuration < count; ++configuration) { + VertexInputLayout input; + input.bindings.push_back({0, 4u * (configuration + 1u), VK_VERTEX_INPUT_RATE_VERTEX}); + bool created = false; + static_cast(CachedPipeline(context, State{}, input, resources, shaders, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, &created)); + built += created ? 1u : 0u; + } + return built; + }; + std::uint32_t lastBuilt = 0; + for (int round = 0; round < 6; ++round) lastBuilt = pass(configurations); + const auto grown = PipelineStoreTotals(); + if (fixed) { + Require(grown.bound == 1024, "a bound set with APS5_PIPELINE_STORE_ENTRIES changed"); + Require(lastBuilt == configurations, "a fixed 1024-entry bound kept pipelines of a 1536-configuration cycle"); + } else { + Require(grown.bound == 2048, "the bound did not grow to 2048 while evicted pipelines kept coming back"); + Require(lastBuilt == 0, "a pass over 1536 configurations still rebuilt pipelines after the bound grew"); + } + ClearCachedPipelines(context.device); + Require(mock.live == 0, "pipeline store growth tests leaked Vulkan objects"); +} + +// A journal longer than the store's starting bound: 1032 recorded pipelines replayed into an empty +// store all stay resident (the replay grows the bound instead of dropping what does not fit). +void pipelineStoreReplayGrowthTests() { + using namespace AgcDriver::Graphics; + const auto root = std::filesystem::temp_directory_path() / ("aps5-pipeline-store-replay-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root); + struct Cleanup { + std::filesystem::path directory; + ~Cleanup() { + ShaderRecompiler::ShaderDiskCache::Shutdown(); + std::error_code error; + std::filesystem::remove_all(directory, error); + } + } cleanup{root}; +#ifdef _WIN32 + _putenv_s("ANYPS5_NO_SHADER_CACHE", "0"); + _wputenv_s(L"ANYPS5_SHADER_CACHE_DIR", root.c_str()); + _putenv_s("APS5_PIPELINE_JOURNAL_THREADS", "1"); +#else + setenv("ANYPS5_NO_SHADER_CACHE", "0", 1); + setenv("ANYPS5_SHADER_CACHE_DIR", root.string().c_str(), 1); + setenv("APS5_PIPELINE_JOURNAL_THREADS", "1", 1); +#endif + Require(PipelineJournalEnabled(), "the journal is disabled with a shader cache directory set"); + mock = MockVulkan{}; + auto context = mockContext(); + context.device = makeHandle(); + context.graphicsPipelineLibrary = true; + context.pipelineLinkTimeOptimization = false; + context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT; + ShaderRecompiler::RecompileResult vertex, fragment; + vertex.spirv = makeModule({}); + fragment.spirv = makeModule({.fragment = true}); + vertex.variantId = 401; + fragment.variantId = 402; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}}; + auto cache = std::make_unique(context); + context.descriptorCache = cache.get(); + const auto layouts = DescribeSetLayouts(context, shaders); + const auto resources = layouts.View(); + constexpr std::uint64_t fingerprint = 0x5eed'0000'0000'0002ull; + { + auto absent = StartPipelineJournalReplay(context, fingerprint, {}); + Require(absent != nullptr, "the first replay did not start"); + absent->Wait(); + } + constexpr std::uint32_t configurations = 1032; + for (std::uint32_t configuration = 0; configuration < configurations; ++configuration) { + VertexInputLayout input; + input.bindings.push_back({0, 4u * (configuration + 1u), VK_VERTEX_INPUT_RATE_VERTEX}); + static_cast(CachedPipeline(context, State{}, input, resources, shaders)); + } + FlushPipelineJournal(); + ClearCachedPipelines(context.device); + Require(PipelineStoreTotals().cached == 0, "clearing the device left pipelines in the store"); + auto replay = StartPipelineJournalReplay(context, fingerprint, {}); + Require(replay != nullptr, "the replay did not start"); + replay->Wait(); + const auto totals = PipelineStoreTotals(); + Require(totals.cached == configurations, "a replay of " + std::to_string(configurations) + " recorded pipelines kept " + std::to_string(totals.cached) + " in the store (bound " + std::to_string(totals.bound) + ")"); + ClearCachedPipelines(context.device); + context.descriptorCache = nullptr; + cache.reset(); + Require(mock.live == 0, "pipeline store replay growth tests leaked Vulkan objects"); +} + void pipelineJournalTests(bool longPath = false, bool extended = false) { using namespace AgcDriver::Graphics; using Stage = ShaderRecompiler::ShaderStage; @@ -11809,6 +12206,16 @@ int main(int argc, char** argv) { std::cout << "pipeline library clear waits for active part builds\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--pipeline-store-replay-growth") { + pipelineStoreReplayGrowthTests(); + std::cout << "a replayed journal longer than the store bound stays resident\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--pipeline-store-growth") { + pipelineStoreGrowthTests(std::getenv("APS5_PIPELINE_STORE_ENTRIES") != nullptr); + std::cout << "pipeline store bound follows the evicted pipelines that come back\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--pipeline-store-lazy-buffer-pool") { pipelineStoreLazyBufferPoolTests(); std::cout << "cached pipelines survive lazy buffer pool creation\n"; @@ -11917,6 +12324,32 @@ int main(int argc, char** argv) { Require(mock.live == 0, "the heap mirror run unwatch test leaked Vulkan objects"); return 0; } + if (argc == 2 && (std::string_view(argv[1]) == "--address-copy-cap" || std::string_view(argv[1]) == "--address-copy-cap-remembered")) { +#ifdef _WIN32 + _putenv_s("APS5_ADDRESS_COPY_MAX_MIB", "1"); +#else + setenv("APS5_ADDRESS_COPY_MAX_MIB", "1", 1); +#endif + { + mock = MockVulkan{}; + auto context = mockContext(); + context.bufferDeviceAddress = true; + context.limits.maxStorageBufferRange = 1u << 27; + RunAddressCopyCapTests(context, { + [](VkBuffer buffer) -> std::span { return mockBufferMemory(buffer); }, + {}, + [](VkDeviceAddress address) { + const auto offset = address - 0x100000000000ULL; + const auto buffer = reinterpret_cast(offset / 0x10000); + return mockBufferMemory(buffer).subspan(offset % 0x10000); + }, + {}, + {} + }, std::string_view(argv[1]) == "--address-copy-cap-remembered"); + } + Require(mock.live == 0, "the address copy cap test leaked Vulkan objects"); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--address-space-teardown") { addressSpaceTeardownTests(); std::cout << "address-space device teardown releases cached guest allocations\n"; @@ -12044,6 +12477,7 @@ int main(int argc, char** argv) { retainedPixelStageTests(); pipelineLibrarySetTests(); pipelineLibraryClearWaitsForBuildTests(); + tessellationPipelineLibraryCacheTests(); framebufferOwnerTests(); hdrComponentSwapTests(); depthMaintenanceTests(); @@ -12051,6 +12485,7 @@ int main(int argc, char** argv) { srgb8TargetTests(); DepthClipTests(); DepthRemapTests(); + DepthBoundsEmulationTests(); UnusedViewportDepthTests(); DepthStencilTests(); OneDimensionalColorTests(); @@ -12061,12 +12496,14 @@ int main(int argc, char** argv) { unorm10_11_11TargetTests(); float11_11_10TargetTests(); unorm4_4_4_4TargetTests(); + uintTargetTests(); DepthBoundsBiasTests(); conservativeZExportTests(); earlyFragmentTestsTests(); orderedPixelShaderTests(); stencilRefExportTests(); stencilReferenceUnusedTests(); + stencilLogicOperationTests(); aliasedBufferViewTests(); LineStripTests(); ShaderModuleKeyTests(); @@ -12136,6 +12573,7 @@ int main(int argc, char** argv) { samplerTableDeviceLimitTests(); tableUpdateAfterBindTests(); misalignedShaderDataTests(); + imageTableTemplateRefreshTests(); debugBranchTests(); hostImportBudgetTests(); textureCacheBudgetTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp b/core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp index e20b23d8d5..452bac82c4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp @@ -32,7 +32,7 @@ struct LoadCase { std::uint32_t components = 1; }; -std::vector emitLoad(const LoadCase& test, bool active, std::uint32_t offset, std::uint32_t records, bool robust) { +std::vector emitLoad(const LoadCase& test, bool active, std::uint32_t offset, std::uint32_t records, bool robust, bool valid = true) { IrProgram program; program.SetWaveSize(32); program.Resources().stage = IrShaderStage::Compute; @@ -76,7 +76,8 @@ std::vector emitLoad(const LoadCase& test, bool active, std::uint program.Metadata().bindings.pushDataStartDword = 0; program.Metadata().bindings.memoryOffsetCount = 1; auto& size = constant(records); - auto& handle = emit(IrOpcode::GetBufferResource, IrType::BufferResource, {&zero, &zero, &size, &zero}); + auto& base = constant(valid ? 0x100u : 0u); + auto& handle = emit(IrOpcode::GetBufferResource, IrType::BufferResource, {&base, &zero, &size, &zero}); emit(test.opcode, test.type, {&handle, &zero, &address, &zero, &enabled}); } else { if (test.kind == ResourceKind::Gds) program.Metadata().bindings.descriptors.push_back({DescriptorBindingKind::Gds, {0}}); @@ -104,8 +105,8 @@ std::vector emitLoad(const LoadCase& test, bool active, std::uint class AccessTrace { public: - AccessTrace(const std::vector& module, std::uint32_t misalignment = 0, bool valid = true) - : words(module), misalignment(misalignment), valid(valid) { + AccessTrace(const std::vector& module, std::uint32_t misalignment = 0) + : words(module), misalignment(misalignment) { require(words.size() >= 5, "SPIR-V module header is missing"); for (std::size_t offset = 5; offset < words.size();) { const auto count = words[offset] >> 16; @@ -206,7 +207,6 @@ private: const auto spec = specializations.at(id); if (spec == PipelineSpecialization::BufferBase) return 0; if (spec == PipelineSpecialization::BufferBase + 1) return 0x30001000; - if (spec == PipelineSpecialization::BufferBase + 2) return valid; if (spec == PipelineSpecialization::BufferBase + 3) return misalignment; if (spec == PipelineSpecialization::PushDataOffset) return 0; throw std::runtime_error("unexpected specialization in access fixture"); @@ -250,7 +250,6 @@ private: const std::vector& words; std::uint32_t misalignment; - bool valid; std::uint32_t entryFunction = 0; std::map> definitions; std::map names; @@ -281,8 +280,8 @@ std::uint32_t ldsLength(const std::vector& words) { } void expectReads(const LoadCase& test, bool robust, bool active, std::uint32_t offset, std::vector expected, std::uint32_t records = 64, std::uint32_t misalignment = 0, bool valid = true) { - const auto module = emitLoad(test, active, offset, records, robust); - const auto actual = AccessTrace(module, misalignment, valid).Run(); + const auto module = emitLoad(test, active, offset, records, robust, valid); + const auto actual = AccessTrace(module, misalignment).Run(); if (actual != expected) { std::string detail = std::string(robust ? "robust" : "plain") + " guest reads:"; for (const auto index : actual) detail += " " + std::to_string(index); diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index a77f88d58f..606af7d757 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -1074,6 +1074,54 @@ int main() { return !StencilReplaySafe(b.program); }); + const auto wholeQuadEval = [](Builder& b, IrValue& live) -> IrValue& { + auto& ballot = b.emit(IrOpcode::Ballot, IrType::U32x4, {&live}); + auto& allLow = b.emit(IrOpcode::Ballot, IrType::U32x4, {&b.truth()}); + auto& allHigh = b.emit(IrOpcode::Ballot, IrType::U32x4, {&b.truth()}); + auto& low = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &b.constant(0u)}), &b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&allLow, &b.constant(0u)})}); + auto& high = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &b.constant(1u)}), &b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&allHigh, &b.constant(1u)})}); + return b.emit(IrOpcode::WqmU64, IrType::U64, {&b.emit(IrOpcode::CompositeConstructU64, IrType::U64, {&low, &high})}); + }; + + const auto splitQuads = [&](Builder& b, IrValue& live) -> IrValue& { + auto& wqmLow = wholeQuadEval(b, live); + auto& wqmHigh = wholeQuadEval(b, live); + return b.threadBit(b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqmLow, &b.constant(0u)}), b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqmHigh, &b.constant(1u)})); + }; + + passed &= run("stencil replay pairs structurally equivalent whole-quad evaluations for split low and high words", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + exportUnder(b, splitQuads(b, b.logicalAnd(pixelPredicate(b), helperPredicate(b, false))), true, true); + return StencilReplaySafe(b.program); + }); + + passed &= run("stencil replay refuses to pair whole-quad evaluations separated by an execution-mask change", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& live = b.logicalAnd(pixelPredicate(b), helperPredicate(b, false)); + auto& wqmLow = wholeQuadEval(b, live); + b.emit(IrOpcode::SetExecLo, IrType::Void, {&b.constant(0xffffffffu)}); + auto& wqmHigh = wholeQuadEval(b, live); + auto& guard = b.threadBit(b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqmLow, &b.constant(0u)}), b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqmHigh, &b.constant(1u)})); + exportUnder(b, guard, true, true); + return !StencilReplaySafe(b.program); + }); + + passed &= run("stencil replay refuses to pair whole-quad evaluations whose predicates differ only in a constant", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& helperLow = b.emit(IrOpcode::GetBuiltin, IrType::U32, {&b.constant(static_cast(StageInputKind::HelperInvocation)), &b.constant(0u)}); + auto& lowLive = b.logicalAnd(pixelPredicate(b), b.emit(IrOpcode::IEqual32, IrType::Bool, {&helperLow, &b.constant(0u)})); + auto& wqmLow = wholeQuadEval(b, lowLive); + auto& helperHigh = b.emit(IrOpcode::GetBuiltin, IrType::U32, {&b.constant(static_cast(StageInputKind::HelperInvocation)), &b.constant(0u)}); + auto& highLive = b.logicalAnd(pixelPredicate(b), b.emit(IrOpcode::IEqual32, IrType::Bool, {&helperHigh, &b.constant(1u)})); + auto& wqmHigh = wholeQuadEval(b, highLive); + auto& guard = b.threadBit(b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqmLow, &b.constant(0u)}), b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqmHigh, &b.constant(1u)})); + exportUnder(b, guard, true, true); + return !StencilReplaySafe(b.program); + }); + const auto execCopy = [](Builder& b, IrValue& low, IrValue& high) -> IrValue& { auto& active = b.emit(IrOpcode::Ballot, IrType::U32x4, {&b.truth()}); auto& lanes = b.emit(IrOpcode::CompositeConstructU64, IrType::U64, {&b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&active, &b.constant(0u)}), &b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&active, &b.constant(1u)})}); @@ -1188,10 +1236,205 @@ int main() { return StencilReplaySafe(b.program); }; + const auto optionalPredicateBody = [&](bool negated) { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& entry = *b.block; + auto& predicate = pixelPredicate(b); + auto& condition = b.emit(IrOpcode::LogicalNot, IrType::Bool, {&predicate}); + auto& body = b.newBlock(); + auto& merge = b.newBlock(); + entry.AddBranch(&body); + entry.AddBranch(&merge); + body.AddBranch(&merge); + auto& entryInfo = b.program.Metadata().blockInfo[0u]; + entryInfo.terminator.kind = TerminatorKind::ConditionalBranch; + entryInfo.terminator.condition = BranchCondition::ExecZero; + entryInfo.terminator.trueBlock = merge.Id(); + entryInfo.terminator.falseBlock = body.Id(); + entryInfo.condition = &condition; + auto& bodyInfo = b.program.Metadata().blockInfo[1u]; + bodyInfo.terminator.kind = TerminatorKind::Branch; + bodyInfo.terminator.trueBlock = merge.Id(); + b.block = &body; + auto& written = negated ? b.emit(IrOpcode::LogicalNot, IrType::Bool, {&predicate}) : b.ballotBit(predicate); + b.block = &merge; + auto& merged = b.phi(IrType::Bool, entry, predicate); + merged.AddPhiOperand(&body, &written); + b.keep(b.select(merged, b.constant(1u), b.constant(0u))); + exportUnder(b, b.truth(), true, true); + return StencilReplaySafe(b.program); + }; + + passed &= run("an optional wave body may merge a lane's own bit of the predicate's ballot with the skipped predicate", [&] { return optionalPredicateBody(false); }); + passed &= run("an optional wave body cannot merge a value that differs from the skipped predicate on inactive lanes", [&] { return !optionalPredicateBody(true); }); passed &= run("an optional wave body preserves inactive values consumed only within a lane", [&] { return optionalPixelBody(false); }); passed &= run("an optional wave body cannot change helper coordinates for later implicit samples", [&] { return !optionalPixelBody(true); }); passed &= run("an optional wave body cannot change helper coordinates for LOD queries", [&] { return !optionalPixelBody(true, IrOpcode::ImageQueryLod); }); + const auto optionalPixelRegion = [&](bool unmaskedArm) { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& entry = *b.block; + auto& predicate = pixelPredicate(b); + auto& zero = b.constant(0u); + auto& condition = b.emit(IrOpcode::LogicalNot, IrType::Bool, {&predicate}); + auto& body = b.newBlock(); + auto& left = b.newBlock(); + auto& right = b.newBlock(); + auto& join = b.newBlock(); + auto& merge = b.newBlock(); + entry.AddBranch(&body); + entry.AddBranch(&merge); + body.AddBranch(&left); + body.AddBranch(&right); + left.AddBranch(&join); + right.AddBranch(&join); + join.AddBranch(&merge); + auto& entryInfo = b.program.Metadata().blockInfo[0u]; + entryInfo.terminator.kind = TerminatorKind::ConditionalBranch; + entryInfo.terminator.condition = BranchCondition::ExecZero; + entryInfo.terminator.trueBlock = merge.Id(); + entryInfo.terminator.falseBlock = body.Id(); + entryInfo.condition = &condition; + b.block = &body; + auto& uniformBit = b.emit(IrOpcode::INotEqual32, IrType::Bool, {&b.emit(IrOpcode::GetUserData, IrType::U32, {}), &zero}); + auto& bodyInfo = b.program.Metadata().blockInfo[1u]; + bodyInfo.terminator.kind = TerminatorKind::ConditionalBranch; + bodyInfo.terminator.condition = BranchCondition::SccNonZero; + bodyInfo.terminator.trueBlock = left.Id(); + bodyInfo.terminator.falseBlock = right.Id(); + bodyInfo.condition = &uniformBit; + for (const unsigned index : {2u, 3u, 4u}) { + b.program.Metadata().blockInfo[index].terminator.kind = TerminatorKind::Branch; + b.program.Metadata().blockInfo[index].terminator.trueBlock = index == 4u ? merge.Id() : join.Id(); + } + b.block = &left; + auto& leftValue = b.select(predicate, b.constant(42u), zero); + b.block = &right; + auto& rightValue = unmaskedArm ? b.constant(7u) : b.select(predicate, b.constant(43u), zero); + b.block = &join; + auto& joined = b.phi(IrType::U32, left, leftValue); + joined.AddPhiOperand(&right, &rightValue); + b.block = &merge; + auto& merged = b.phi(IrType::U32, entry, zero); + merged.AddPhiOperand(&join, &joined); + b.keep(merged); + exportUnder(b, b.truth(), true, true); + return StencilReplaySafe(b.program); + }; + + passed &= run("an optional wave body spanning several blocks preserves inactive values through its own phis", [&] { return optionalPixelRegion(false); }); + passed &= run("an optional wave body spanning several blocks cannot write an inactive lane on any of its paths", [&] { return !optionalPixelRegion(true); }); + + const auto nestedOptionalGuard = [&](bool unsafeInner, bool ballotEscape) { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& entry = *b.block; + auto& predicate = pixelPredicate(b); + auto& condition = b.emit(IrOpcode::LogicalNot, IrType::Bool, {&predicate}); + auto& base = helperPredicate(b, false); + auto& body = b.newBlock(); + auto& merge = b.newBlock(); + auto& inner = b.newBlock(); + auto& join = b.newBlock(); + entry.AddBranch(&body); + entry.AddBranch(&merge); + body.AddBranch(&merge); + merge.AddBranch(&inner); + merge.AddBranch(&join); + inner.AddBranch(&join); + auto& entryInfo = b.program.Metadata().blockInfo[0u]; + entryInfo.terminator.kind = TerminatorKind::ConditionalBranch; + entryInfo.terminator.condition = BranchCondition::ExecZero; + entryInfo.terminator.trueBlock = merge.Id(); + entryInfo.terminator.falseBlock = body.Id(); + entryInfo.condition = &condition; + b.program.Metadata().blockInfo[1u].terminator.kind = TerminatorKind::Branch; + b.program.Metadata().blockInfo[1u].terminator.trueBlock = merge.Id(); + b.block = &body; + auto& taken = b.emit(IrOpcode::SelectU1, IrType::Bool, {&predicate, &b.truth(), &base}); + b.block = &merge; + auto& merged = b.phi(IrType::Bool, entry, base); + merged.AddPhiOperand(&body, &taken); + auto& innerCondition = b.emit(IrOpcode::LogicalNot, IrType::Bool, {&merged}); + auto& mergeInfo = b.program.Metadata().blockInfo[2u]; + mergeInfo.terminator.kind = TerminatorKind::ConditionalBranch; + mergeInfo.terminator.condition = BranchCondition::ExecZero; + mergeInfo.terminator.trueBlock = join.Id(); + mergeInfo.terminator.falseBlock = inner.Id(); + mergeInfo.condition = &innerCondition; + b.program.Metadata().blockInfo[3u].terminator.kind = TerminatorKind::Branch; + b.program.Metadata().blockInfo[3u].terminator.trueBlock = join.Id(); + b.block = &inner; + auto& written = b.select(merged, b.constant(42u), b.constant(0u)); + if (unsafeInner) b.emit(IrOpcode::Discard, IrType::Void, {}); + b.block = &join; + auto& joined = b.phi(IrType::U32, merge, b.constant(0u)); + joined.AddPhiOperand(&inner, &written); + if (ballotEscape) { + const auto [low, high] = b.ballotWords(merged); + b.keep(b.threadBit(*low, *high)); + b.keep(b.add(*low, 1u)); + } + b.keep(joined); + exportUnder(b, b.truth(), true, true); + return StencilReplaySafe(b.program); + }; + + passed &= run("an optional wave body merge may guard another optional wave body", [&] { return nestedOptionalGuard(false, false); }); + passed &= run("an optional wave body merge cannot guard a body with side effects", [&] { return !nestedOptionalGuard(true, false); }); + passed &= run("an optional wave body merge cannot feed a ballot escaping the lane's own bit", [&] { return !nestedOptionalGuard(false, true); }); + + passed &= run("an optional wave body merge may feed an own-lane ballot read", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& entry = *b.block; + auto& predicate = pixelPredicate(b); + auto& condition = b.emit(IrOpcode::LogicalNot, IrType::Bool, {&predicate}); + auto& base = helperPredicate(b, false); + auto& body = b.newBlock(); + auto& merge = b.newBlock(); + entry.AddBranch(&body); + entry.AddBranch(&merge); + body.AddBranch(&merge); + auto& entryInfo = b.program.Metadata().blockInfo[0u]; + entryInfo.terminator.kind = TerminatorKind::ConditionalBranch; + entryInfo.terminator.condition = BranchCondition::ExecZero; + entryInfo.terminator.trueBlock = merge.Id(); + entryInfo.terminator.falseBlock = body.Id(); + entryInfo.condition = &condition; + b.program.Metadata().blockInfo[1u].terminator.kind = TerminatorKind::Branch; + b.program.Metadata().blockInfo[1u].terminator.trueBlock = merge.Id(); + b.block = &body; + auto& taken = b.emit(IrOpcode::SelectU1, IrType::Bool, {&predicate, &b.truth(), &base}); + b.block = &merge; + auto& merged = b.phi(IrType::Bool, entry, base); + merged.AddPhiOperand(&body, &taken); + b.keep(b.ballotBit(merged)); + exportUnder(b, b.truth(), true, true); + return StencilReplaySafe(b.program); + }); + + passed &= run("an active mask may flag a sample with an explicit lod", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + MemoryInfo memory{}; + memory.kind = ResourceKind::Image; + memory.imageSampleFlags = RdnaImageSampleFlagLevelZero; + b.program.Resources().memoryInfo.push_back(memory); + auto& active = b.ballotBit(b.truth()); + auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&b.constant(0u)}); + auto& image = b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}); + auto& sampler = b.emit(IrOpcode::GetSamplerResource, IrType::SamplerResource, {}); + MemoryFlags flags{0u, 0u}; + std::uint64_t bits = 0; + std::memcpy(&bits, &flags, sizeof(flags)); + b.emit(IrOpcode::ImageSampleRaw, IrType::U32x4, {&image, &sampler, &address, &active}, bits); + exportUnder(b, b.truth(), true, true); + return StencilReplaySafe(b.program); + }); + passed &= run("varying attribute sources cannot be treated as uniform any-active predicates", [&] { for (const auto opcode : {IrOpcode::GetAttribute, IrOpcode::GetInterpolationParameter, IrOpcode::GetInterpolationParameterF16, IrOpcode::GetTessellationAttribute}) { Builder b; diff --git a/core/libs/prx/libSceAgcDriver/tests/NopDataWritePacket.cpp b/core/libs/prx/libSceAgcDriver/tests/NopDataWritePacket.cpp index d3e4092bd2..6be7c6011a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/NopDataWritePacket.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/NopDataWritePacket.cpp @@ -33,5 +33,18 @@ int main() { std::memcpy(&address, storage.data() + 8, sizeof(address)); if (zeroBytesWords != 2 || header != 0xc0001000U || address != 0x01020304U) return 7; if (storage[12] != std::byte{0xaa}) return 8; + + for (std::uint32_t len = 1; len <= 4; ++len) { + storage.fill(std::byte{0xaa}); + const auto lenWords = sceAgcDriverNopDataWritePacket(storage.data() + 4, 0xaabbccddU, data.data(), len); + std::memcpy(&header, storage.data() + 4, sizeof(header)); + std::memcpy(&address, storage.data() + 8, sizeof(address)); + if (lenWords != 3 || header != 0xc0011000U || address != 0xaabbccddU) return 9 + static_cast(len); + if (!std::equal(data.begin(), data.begin() + len, storage.begin() + 12)) return 14 + static_cast(len); + for (std::size_t pad = 12 + len; pad < 16; ++pad) { + if (storage[pad] != std::byte{0}) return 19 + static_cast(len); + } + if (storage[3] != std::byte{0xaa} || storage[16] != std::byte{0xaa}) return 24 + static_cast(len); + } return 0; } diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp index efad88d641..a6272db6c6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp @@ -134,6 +134,20 @@ void EscapeStrings() { Check(output == "\"a\\\"b\\\\c\\u000a\\u00e9\"", "string escaping is not valid JSON"); } +void Utf8Strings() { + std::string output; + TraceFormat::AppendString(output, "Ghost of Y\xc5\x8dtei \xe2\x84\xa2 \xf0\x9f\x8e\xae"); + Check(output == "\"Ghost of Y\xc5\x8dtei \xe2\x84\xa2 \xf0\x9f\x8e\xae\"", "a UTF-8 title was not written as its own characters"); + output.clear(); + TraceFormat::AppendString(output, "\xc0\xaf\xed\xa0\x80\xf4\x90\x80\x80\xe2\x84\x7f\xc5"); + Check(output == "\"\\u00c0\\u00af\\u00ed\\u00a0\\u0080\\u00f4\\u0090\\u0080\\u0080\\u00e2\\u0084\\u007f\\u00c5\"", "a byte outside a valid UTF-8 sequence was not escaped"); + AgcDriver::TraceSummary summary; + summary.title = AgcDriver::TraceTitle{"PPSA26344", "Ghost of Y\xc5\x8dtei", "01.000.000"}; + const auto origin = Clock::now(); + const std::vector presents{origin, origin + milliseconds(16)}; + Check(TraceFormat::Summary(summary, {}, presents).find("\"title\":\"Ghost of Y\xc5\x8dtei\"") != std::string::npos, "the summary line did not keep a UTF-8 title"); +} + void EventsShape() { const auto origin = Clock::now(); const std::vector slices{ @@ -576,6 +590,7 @@ int main() { FullBufferEvents(); FoldShortSlices(); EscapeStrings(); + Utf8Strings(); EventsShape(); BatchIds(); SummaryFields(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index a95329f86d..5bee52df52 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -2010,6 +2010,41 @@ void testWaitRegMem64ZeroHighMaskDrain() { check(outstanding == 1, "WAIT_REG_MEM_64 with zero high mask at 4 mod 8 skipped the drain of a pending write to the dword it compares"); } +void testWaitRegMemMisalignedBehindBusyPipeline() { + alignas(8) static std::uint64_t target = 0; + alignas(8) static std::uint32_t other = 0; + static std::uint32_t done = 0; + const auto base = reinterpret_cast(&target); + const auto otherAddr = reinterpret_cast(&other); + const std::vector waits[]{ + makePacket(0x3c, {0x13, static_cast(base + 1), static_cast((base + 1) >> 32u), 42, 0xffffffffu, 0}), + makePacket(0x93, {0x13, static_cast(base + 3), static_cast((base + 3) >> 32u), 42, 0, 0xffffffffu, 0xffffffffu, 0})}; + for (const auto& wait : waits) { + std::atomic_ref(target).store(0); + std::atomic_ref(done).store(0); + std::atomic hold{true}; + std::atomic started{false}; + AgcDriver::DriverDetail::DrawPipeline::Queue0().Enqueue([&] { + started.store(true); + while (hold.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + }, { {otherAddr, otherAddr + 4} }); + while (!started.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + const auto opcode = (wait[0] >> 8u) & 0xffu; + const auto drainsBefore = AgcDriver::DriverDetail::DrawPipeline::DrainRequestsByOpcode(opcode); + auto words = joinPackets({wait, writeWord(done, 1)}); + submitWords(words, 0); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (AgcDriver::DriverDetail::DrawPipeline::DrainRequestsByOpcode(opcode) == drainsBefore && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + const bool drained = AgcDriver::DriverDetail::DrawPipeline::DrainRequestsByOpcode(opcode) != drainsBefore; + std::atomic_ref(target).store(42); + hold.store(false); + AgcDriverWaitIdle_nid_postfix(); + waitDrawPipelineIdle(); + check(drained, "unsatisfied misaligned WAIT_REG_MEM behind a busy draw pipeline did not drain it"); + check(std::atomic_ref(done).load() == 1, "misaligned WAIT_REG_MEM behind a busy draw pipeline did not finish"); + } +} + int main(int argc, char** argv) { try { if (argc == 2 && std::string(argv[1]) == "failure") { @@ -2086,6 +2121,7 @@ int main(int argc, char** argv) { testWaitRegMemMasking(); testWaitRegMemPipelineDrain(); testWaitRegMem64ZeroHighMaskDrain(); + testWaitRegMemMisalignedBehindBusyPipeline(); LibcRunShutdown_nid_postfix(); std::puts("PM4 catalog, registers, state, memory, conditional execution and submission tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp b/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp index 11915c8575..4449d50b9a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp @@ -219,13 +219,21 @@ struct Source { std::vector decoded; }; -Source makeSource(std::uint64_t pixelFormat, VkFormat storage, std::uint32_t width, std::uint32_t height, std::uint32_t salt = 0) { +Source makeSource(std::uint64_t pixelFormat, VkFormat storage, std::uint32_t width, std::uint32_t height, std::uint32_t salt = 0, bool premultiplied = false) { Source source{pixelFormat, storage, width, height, std::vector(static_cast(width) * height * 4), {}}; std::uint32_t seed = 0x9e3779b9u ^ static_cast(storage) ^ static_cast(pixelFormat >> 32u) ^ salt; for (auto& byte : source.words) { seed = seed * 1664525u + 1013904223u; byte = static_cast(seed >> 24u); } + if (premultiplied) { + source.words[3] = std::byte{0}; + source.words[7] = std::byte{255}; + for (std::size_t texel = 0; texel < source.words.size(); texel += 4) { + const auto alpha = std::to_integer(source.words[texel + 3]); + for (std::size_t channel = 0; channel < 3; ++channel) source.words[texel + channel] = static_cast(std::to_integer(source.words[texel + channel]) * alpha / 255u); + } + } const ColorTargetLayout layout(width, height, ColorTileMode::RenderTarget); std::vector storageBytes(layout.Bytes() + 65536); auto* aligned = reinterpret_cast((reinterpret_cast(storageBytes.data()) + 65535u) & ~std::uintptr_t{65535}); @@ -539,6 +547,182 @@ void passConvertsPq(const Context& context, PresentationPass& pass, const Source } } +std::vector overlayPresent(const Context& context, PresentationPass& pass, PresentationPass& planes, const Source& main, const Image* mainImage, const Source& overlay, const Image* overlayImage, VkExtent2D extent) { + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT); + const auto bytes = static_cast(extent.width) * extent.height * 4; + Buffer readback(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + VkImageLayout loaded = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + if (mainImage != nullptr) { + pass.Bind(0, mainImage->Handle(), main.width, main.height, main.pixelFormat, AgcDriver::SdrToneMapLuminanceNits); + pass.RecordPresent(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, loaded); + } else { + loaded = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, loaded, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + const VkClearColorValue color{{64.0f / 255.0f, 128.0f / 255.0f, 191.0f / 255.0f, 1.0f}}; + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdClearColorImage")(commands, destination.Handle(), loaded, &color, 1, &range); + } + if (overlayImage != nullptr) planes.Bind(1, overlayImage->Handle(), overlay.width, overlay.height, overlay.pixelFormat, AgcDriver::SdrToneMapLuminanceNits); + else planes.BindTexels(1, commands, overlay.words, overlay.width, overlay.height, overlay.pixelFormat, AgcDriver::SdrToneMapLuminanceNits); + planes.RecordOverlay(commands, 1, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, loaded, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + readImage(context, commands, destination, readback); + batch.SubmitAndWait(); + } + pass.Release(0); + planes.Release(1); + return bytesOf(readback, bytes); +} + +std::vector clearedFrame(VkExtent2D extent) { + std::vector frame(static_cast(extent.width) * extent.height * 4); + for (std::size_t offset = 0; offset < frame.size(); offset += 4) { + frame[offset] = std::byte{191}; + frame[offset + 1] = std::byte{128}; + frame[offset + 2] = std::byte{64}; + frame[offset + 3] = std::byte{255}; + } + return frame; +} + +void overlayBlendsPremultiplied(const Context& context, PresentationPass& pass, PresentationPass& planes, const Source& main, const Image& mainImage, const Source& overlay, const Image& overlayImage) { + for (const auto extent : {VkExtent2D{overlay.width, overlay.height}, VkExtent2D{overlay.width + 60, overlay.height}, VkExtent2D{overlay.width, overlay.height + 51}, VkExtent2D{117, 76}, VkExtent2D{331, 215}}) { + const auto rect = AgcDriver::ComputeContainRect_nid_postfix(overlay.width, overlay.height, extent.width, extent.height); + const bool exact = rect.width == overlay.width && rect.height == overlay.height; + const auto alone = passPresent(context, pass, overlay, overlayImage, extent, VK_FORMAT_B8G8R8A8_UNORM); + for (const bool cleared : {false, true}) { + const auto base = cleared ? clearedFrame(extent) : passPresent(context, pass, main, mainImage, extent, VK_FORMAT_B8G8R8A8_UNORM); + for (const bool uploaded : {false, true}) { + const auto blended = overlayPresent(context, pass, planes, main, cleared ? nullptr : &mainImage, overlay, uploaded ? nullptr : &overlayImage, extent); + int worst = 0; + for (std::uint32_t y = 0; y < extent.height; ++y) { + for (std::uint32_t x = 0; x < extent.width; ++x) { + const auto offset = (static_cast(y) * extent.width + x) * 4; + const bool inside = static_cast(x) >= rect.x && static_cast(x) < rect.x + static_cast(rect.width) && static_cast(y) >= rect.y && static_cast(y) < rect.y + static_cast(rect.height); + for (std::size_t channel = 0; channel < 4; ++channel) { + int expected = std::to_integer(base[offset + channel]); + if (inside && channel < 3) { + const double alpha = std::to_integer(alone[offset + 3]) / 255.0; + expected = static_cast(std::min(255.0, std::to_integer(alone[offset + channel]) + expected * (1.0 - alpha)) + 0.5); + } + const int difference = std::abs(expected - std::to_integer(blended[offset + channel])); + Require(inside || difference == 0, "the overlay plane changed the destination outside its rectangle: " + describe(overlay, extent)); + worst = std::max(worst, difference); + } + } + } + std::printf(" overlay %s over %s, %s: by at most %d\n", describe(overlay, extent).c_str(), cleared ? "a cleared destination" : "the main plane", uploaded ? "uploaded texels" : "resident image", worst); + Require(worst <= (exact ? 1 : 2), "the overlay plane is not blended with its premultiplied alpha: " + describe(overlay, extent)); + } + } + } +} + +int blendedDifference(const std::vector& base, const std::vector& alone, const std::vector& blended) { + Require(base.size() == alone.size() && base.size() == blended.size(), "compared frames differ in size"); + int worst = 0; + for (std::size_t offset = 0; offset < base.size(); offset += 4) { + const double alpha = std::to_integer(alone[offset + 3]) / 255.0; + for (std::size_t channel = 0; channel < 4; ++channel) { + int expected = std::to_integer(base[offset + channel]); + if (channel < 3) expected = static_cast(std::min(255.0, std::to_integer(alone[offset + channel]) + expected * (1.0 - alpha)) + 0.5); + worst = std::max(worst, std::abs(expected - std::to_integer(blended[offset + channel]))); + } + } + return worst; +} + +void overlayBlendsIntoDumps(const Context& context, PresentationPass& pass, PresentationPass& planes, const Source& main, const Image& mainImage, const Source& overlay, const Image& overlayImage) { + for (const std::uint32_t stride : {1u, 3u}) { + const auto extent = PresentationPass::DumpExtent(main.width, main.height, stride); + const auto base = passDump(context, pass, main, mainImage, stride); + const auto alone = passPresent(context, pass, overlay, overlayImage, extent, VK_FORMAT_B8G8R8A8_UNORM); + const auto bytes = static_cast(extent.width) * extent.height * 4; + Buffer readback(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + pass.Bind(1, mainImage.Handle(), main.width, main.height, main.pixelFormat, AgcDriver::SdrToneMapLuminanceNits); + planes.Bind(1, overlayImage.Handle(), overlay.width, overlay.height, overlay.pixelFormat, AgcDriver::SdrToneMapLuminanceNits); + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + pass.RecordDumpImage(commands, 1, stride); + Require(pass.DumpImageExtent(1).width == extent.width && pass.DumpImageExtent(1).height == extent.height, "the dump image extent differs from the dump extent"); + planes.RecordOverlay(commands, 1, pass.DumpImage(1), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + pass.RecordDumpReadback(commands, 1, readback.Handle()); + batch.SubmitAndWait(); + } + pass.Release(1); + planes.Release(1); + const int worst = blendedDifference(base, alone, bytesOf(readback, bytes)); + std::printf(" overlay %s blended into the presentation dump at stride %u: by at most %d\n", describe(overlay, extent).c_str(), stride, worst); + Require(worst <= (stride == 1 ? 1 : 2), "the overlay plane is not blended into the presentation dump: " + describe(overlay, extent)); + } + const VkExtent2D extent{main.width, main.height}; + PresentationScaler scaler(context, VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM); + scaler.EnsureSourceImage(main.width, main.height); + Buffer upload(context, main.decoded.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::memcpy(upload.Bytes().data(), main.decoded.data(), main.decoded.size()); + const auto bytes = main.decoded.size(); + Buffer readback(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const auto alone = passPresent(context, pass, overlay, overlayImage, extent, VK_FORMAT_B8G8R8A8_UNORM); + planes.Bind(1, overlayImage.Handle(), overlay.width, overlay.height, overlay.pixelFormat, AgcDriver::SdrToneMapLuminanceNits); + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + scaler.RecordUpload(commands, upload.Handle()); + planes.RecordOverlay(commands, 1, scaler.SourceImage(), scaler.SourceFormat(), VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + scaler.RecordReadback(commands, readback.Handle()); + batch.SubmitAndWait(); + } + planes.Release(1); + const int worst = blendedDifference(main.decoded, alone, bytesOf(readback, bytes)); + std::printf(" overlay %s blended into the scaler source: by at most %d\n", describe(overlay, extent).c_str(), worst); + Require(worst <= 1, "the overlay plane is not blended into the scaler source the dump reads back: " + describe(overlay, extent)); +} + +void overlayRefusals(const Context& context, PresentationPass& planes, const Image& overlayImage, const Source& overlay) { + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, {overlay.width, overlay.height}, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + const auto refused = [&](std::uint64_t pixelFormat, VkColorSpaceKHR colorSpace, VkImageLayout initialLayout) { + bool threw = false; + try { + CommandBatch batch(context); + planes.Bind(1, overlayImage.Handle(), overlay.width, overlay.height, pixelFormat, AgcDriver::SdrToneMapLuminanceNits); + planes.RecordOverlay(batch.Handle(), 1, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, colorSpace, {overlay.width, overlay.height}, initialLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + } catch (const std::exception&) { + threw = true; + } + planes.Release(1); + return threw; + }; + Require(refused(Bgra8 | TenBit, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL), "a 10-bit overlay plane was blended without its 2-bit alpha"); + Require(refused(Rgb10Pq, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL), "a PQ overlay plane was blended"); + Require(refused(Bgra8, VK_COLOR_SPACE_HDR10_ST2084_EXT, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL), "an overlay plane was blended into an HDR10 destination"); + Require(refused(Bgra8, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, VK_IMAGE_LAYOUT_UNDEFINED), "an overlay plane was blended over discarded contents"); + bool truncated = false; + try { + CommandBatch batch(context); + planes.BindTexels(1, batch.Handle(), std::span(overlay.words).first(overlay.words.size() - 4), overlay.width, overlay.height, overlay.pixelFormat, AgcDriver::SdrToneMapLuminanceNits); + } catch (const std::exception&) { + truncated = true; + } + Require(truncated, "overlay texels that do not cover their extent were uploaded"); +} + +void overlayPlaneTests(const Context& context) { + PresentationPass pass(context, 2); + PresentationPass planes(context, 2); + const auto main = makeSource(Bgra8, VK_FORMAT_B8G8R8A8_UNORM, 200, 130); + const auto mainImage = residentImage(context, main); + for (const auto& [pixelFormat, storage] : {std::pair{Bgra8, VK_FORMAT_B8G8R8A8_UNORM}, std::pair{Rgba8, VK_FORMAT_R8G8B8A8_UNORM}}) { + const auto overlay = makeSource(pixelFormat, storage, 200, 130, 0x0f0f0f0fu, true); + const auto overlayImage = residentImage(context, overlay); + overlayBlendsPremultiplied(context, pass, planes, main, *mainImage, overlay, *overlayImage); + overlayBlendsIntoDumps(context, pass, planes, main, *mainImage, overlay, *overlayImage); + overlayRefusals(context, planes, *overlayImage, overlay); + } +} + void residentPresentTests(const Context& context) { PresentationPass pass(context, 2); const std::array, 5> cases{{ @@ -568,6 +752,64 @@ void residentPresentTests(const Context& context) { blitLetterboxMatchesClear(context); } +PFN_vkGetDeviceProcAddr realDeviceProc = nullptr; +PFN_vkAllocateMemory realAllocateMemory = nullptr; +const VkPhysicalDeviceMemoryProperties* refusingMemory = nullptr; +std::uint32_t refusedAllocations = 0; +std::uint32_t lastAllocatedType = UINT32_MAX; + +VKAPI_ATTR VkResult VKAPI_CALL refuseDeviceLocalAllocation(VkDevice device, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks* callbacks, VkDeviceMemory* memory) { + const auto heap = refusingMemory->memoryTypes[info->memoryTypeIndex].heapIndex; + if ((refusingMemory->memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0) { + ++refusedAllocations; + return VK_ERROR_OUT_OF_DEVICE_MEMORY; + } + lastAllocatedType = info->memoryTypeIndex; + return realAllocateMemory(device, info, callbacks, memory); +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL refusingDeviceProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast(&refuseDeviceLocalAllocation); + return realDeviceProc(device, name); +} + +void sourceImageOutsideDeviceMemoryTests(const Context& base) { + VkImageCreateInfo probeInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + probeInfo.imageType = VK_IMAGE_TYPE_2D; + probeInfo.format = VK_FORMAT_B8G8R8A8_UNORM; + probeInfo.extent = {64u, 64u, 1u}; + probeInfo.mipLevels = 1; + probeInfo.arrayLayers = 1; + probeInfo.samples = VK_SAMPLE_COUNT_1_BIT; + probeInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + probeInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + VkImage probe = VK_NULL_HANDLE; + Check(base.Function("vkCreateImage")(base.device, &probeInfo, nullptr, &probe), "vkCreateImage probe"); + VkMemoryRequirements requirements{}; + base.Function("vkGetImageMemoryRequirements")(base.device, probe, &requirements); + base.Function("vkDestroyImage")(base.device, probe, nullptr); + if (!base.HostMemoryType(requirements.memoryTypeBits)) { + std::printf("presentation source in host memory: skipped, the device offers no host memory type for the image\n"); + return; + } + auto context = base; + context.bufferPool.reset(); + realDeviceProc = base.deviceProc; + realAllocateMemory = base.Function("vkAllocateMemory"); + refusingMemory = &context.memory; + refusedAllocations = 0; + lastAllocatedType = UINT32_MAX; + context.deviceProc = refusingDeviceProc; + { + PresentationScaler scaler(context, VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM); + scaler.EnsureSourceImage(64u, 64u); + Require(refusedAllocations != 0, "the presentation source never asked for device-local memory"); + Require(lastAllocatedType != UINT32_MAX && (context.memory.memoryHeaps[context.memory.memoryTypes[lastAllocatedType].heapIndex].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0, "the presentation source was not placed in host memory when device-local memory was refused"); + Require(scaler.SourceWidth() == 64u && scaler.SourceHeight() == 64u, "the presentation source in host memory lost its extent"); + } + context.bufferPool.reset(); +} + } int main() { @@ -582,7 +824,9 @@ int main() { return VulkanTestSkipped; } residentPresentTests(device->GetContext()); - std::cout << "resident present format, presentation pass and letterbox tests passed\n"; + overlayPlaneTests(device->GetContext()); + sourceImageOutsideDeviceMemoryTests(device->GetContext()); + std::cout << "resident present format, presentation pass, overlay plane and letterbox tests passed\n"; return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 58cc914302..412903f163 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -50,6 +50,7 @@ #endif #include #include +#include #else #include #include @@ -67,6 +68,7 @@ #include #include #include +#include #include #include #include @@ -322,6 +324,7 @@ public: } return false; }; + memoryBudget = hasExtension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME); auto bytes = AgcDriver::QueryBdaByteFeatures(context.physical, function("vkGetPhysicalDeviceFeatures2"), available); auto address = AgcDriver::QueryBdaFeatures(context.physical, function("vkGetPhysicalDeviceFeatures2"), available); const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); @@ -516,6 +519,21 @@ public: return properties; } void WaitQueue() const { Check(context.Function("vkQueueWaitIdle")(context.queue), "vkQueueWaitIdle"); } + struct HeapUsage { + std::uint64_t deviceLocal = 0; + std::uint64_t host = 0; + }; + std::optional MemoryUsage() const { + if (!memoryBudget) return std::nullopt; + VkPhysicalDeviceMemoryBudgetPropertiesEXT budget{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2, &budget}; + function("vkGetPhysicalDeviceMemoryProperties2")(context.physical, &properties); + HeapUsage usage; + for (std::uint32_t heap = 0; heap < properties.memoryProperties.memoryHeapCount; ++heap) { + ((properties.memoryProperties.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 ? usage.deviceLocal : usage.host) += budget.heapUsage[heap]; + } + return usage; + } private: template @@ -545,6 +563,7 @@ private: bool graphics = false; std::mutex settleMutex; bool nullDescriptors = false; + bool memoryBudget = false; }; using Kind = Recorder::ReadKind; @@ -2511,6 +2530,297 @@ bool settleStorageImageTests(const Device& device) { std::cout << "a settled import under a storage image tests passed\n"; return true; } + +bool settleInitialDccTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + const auto& base = device.GetContext(); + constexpr std::size_t unit = 65536; + constexpr std::size_t tail = 0x4000; + if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0 || !device.Graphics() || base.hostImportAlignment == 0 || base.hostImportAlignment > tail || base.settleQueue == VK_NULL_HANDLE || !WriteWatched()) { + std::cout << "4x color targets, graphics, host imports within 16 KiB, a settle queue or write watching unavailable: an initial multisampled DCC import over a pending settle not tested\n"; + return false; + } + { + std::lock_guard gpu(GpuMutex()); + PrepareImportWatch(base); + if (!settledImportsWatchPrivate(base)) { + std::cout << "this device compares every Windows import: an initial multisampled DCC import over a pending settle not tested\n"; + return false; + } + } + std::cout << "write watch: Windows private imports stay watched after a settle read; imports under 1 MiB wait for a deferred settle\n"; + for (const std::size_t count : {256u, 257u, 1025u}) { + const auto bytes = count * unit; + auto* block = static_cast(AllocateWatched(bytes, unit)); + Require(block != nullptr, "(m) cannot allocate adjacent pending imports"); + const auto address = reinterpret_cast(block); + struct Release { + const Context& context; + std::uint8_t* block; + std::size_t bytes; + std::size_t registered = 0; + ~Release() { + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < registered; ++index) mutation.Remove(block + index * unit); + } + HostImportFor(context, reinterpret_cast(block), unit); + SetHostImportWindow(0); + ReleaseWatched(block, bytes); + } + } release{base, block, bytes}; + std::memset(block, 0x11, bytes); + SetHostImportWindow(unit); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) { + mutation.Add(block + index * unit, unit, true, true, true); + ++release.registered; + } + } + for (std::size_t index = 0; index < count; ++index) { + const auto at = address + index * unit; + const auto* import = HostImportFor(base, at, unit); + Require(import != nullptr && import->base == at && import->bytes == unit && import->unwatched, + "(m) an adjacent 64 KiB import did not start with its settle pending"); + } + { + std::lock_guard gpu(GpuMutex()); + Require(!Watched(address, bytes), "(m) adjacent pending imports were already watched"); + const auto before = HostImportSettles(); + Require(WatchedAfterSettle(base, address, bytes), + "(m) one synchronous settle left part of " + std::to_string(count) + " adjacent imports unwatched"); + const auto after = HostImportSettles(); + BumpCollectEpoch(); + const auto collected = CollectWrites(address, bytes); + Require(collected != 0, "(m) adjacent settled imports were not collected"); + static_cast(block)[bytes - 1] = 0x22; + CollectWritesUncached(address, bytes); + Require(!UnchangedSince(address, bytes, collected), "(m) a CPU write after the final import settled was missed"); + std::cout << "(m) " << count << " adjacent pending imports settled: " << after.submitted - before.submitted << " reads submitted\n"; + } + } + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + Recorder recorder(context); + recorder.Activate(); + enum class Case { Pending, PartialBlock, ComparedNeighbour, DeferredOff, UnwatchedKeys, PendingFmask, PendingFmaskAlone }; + const auto run = [&](Case kind, std::uint32_t height) { + const bool fmaskAlone = kind == Case::PendingFmaskAlone; + const bool eightSamples = kind == Case::PendingFmask || fmaskAlone; + if (eightSamples && (!base.storageImageMultisample || (base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_8_BIT) == 0)) { + std::cout << "(m) 8x color or multisampled storage images unavailable: an initial FMASK import over a pending settle not tested\n"; + return; + } + const bool partial = kind == Case::PartialBlock || kind == Case::ComparedNeighbour; + ColorTarget target{}; + target.extent = {128, height}; + target.surfaceExtent = target.extent; + target.format = VK_FORMAT_R8G8B8A8_UNORM; + target.elementBytes = 4; + target.samples = eightSamples ? 8 : 4; + target.tileMode = ColorTileMode::RenderTarget; + target.dccPipeAligned = true; + const auto colorBytes = static_cast(MultisampledColorBytes(target)); + const auto dccBytes = MultisampledDccBytes(target); + const auto importBytes = colorBytes + (partial ? tail : unit); + const auto reserved = (importBytes + unit - 1) & ~(unit - 1); + const auto fmaskBytes = static_cast(target.extent.width) * target.extent.height * 4u; + const auto fmaskReserved = (fmaskBytes + unit - 1) & ~(unit - 1); + const auto regionBytes = unit + reserved + unit + (kind == Case::UnwatchedKeys ? unit : eightSamples ? fmaskReserved + unit : 0); + auto* region = static_cast(AllocateWatched(regionBytes, unit)); + void* block = region != nullptr ? region + unit : nullptr; + void* keys = region != nullptr && kind == Case::UnwatchedKeys ? region + unit + reserved + unit : nullptr; + void* fmask = region != nullptr && eightSamples ? region + unit + reserved + unit : nullptr; + const auto destBytes = static_cast(target.extent.width) * target.extent.height * 4u; + void* dest = VirtualAlloc(nullptr, destBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + Require(block != nullptr && dest != nullptr && (kind != Case::UnwatchedKeys || keys != nullptr) && (!eightSamples || fmask != nullptr), "(m) cannot allocate the target"); + const auto address = reinterpret_cast(block); + const auto neighbour = address + importBytes + tail; + const auto describe = [](const HostImport* found, std::uint64_t at, std::size_t bytes) { + char text[256]; + if (found == nullptr) std::snprintf(text, sizeof(text), ": no import of 0x%llx+0x%zx, watched %d", static_cast(at), bytes, Watched(at, bytes) ? 1 : 0); + else std::snprintf(text, sizeof(text), ": import 0x%llx+0x%llx, unwatched %d, settled on demand %d, for 0x%llx+0x%zx, watched %d", static_cast(found->base), static_cast(found->bytes), found->unwatched ? 1 : 0, found->settleOnDemand ? 1 : 0, static_cast(at), bytes, Watched(at, bytes) ? 1 : 0); + return std::string(text); + }; + struct Release { + const Context& context; + Recorder& recorder; + void* region; + std::size_t regionBytes; + void* block; + void* keys; + void* fmask; + void* dest; + std::size_t reserved; + std::uint64_t neighbour; + ~Release() { + { + std::lock_guard gpu(GpuMutex()); + recorder.Sync(); + ClearMultisampleTargets(context.device); + ClearCachedTextures(context.device); + recorder.FlushDeferredReleases(); + } + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + mutation.Remove(dest); + if (keys != nullptr) mutation.Remove(keys); + if (fmask != nullptr) mutation.Remove(fmask); + if (neighbour < reinterpret_cast(block) + reserved) mutation.Remove(reinterpret_cast(neighbour)); + } + HostImportFor(context, reinterpret_cast(block), 16); + SetHostImportDeferredSettles(true); + ReleaseWatched(region, regionBytes); + VirtualFree(dest, 0, MEM_RELEASE); + } + } release{context, recorder, region, regionBytes, block, keys, fmask, dest, reserved, kind == Case::ComparedNeighbour ? neighbour : std::numeric_limits::max()}; + target.address = address; + target.surfaceAddress = address; + target.bytes = colorBytes; + target.dccAddress = kind == Case::UnwatchedKeys ? reinterpret_cast(keys) : address + colorBytes; + std::memset(block, 0, importBytes); + std::memset(reinterpret_cast(target.dccAddress), 0xc0, dccBytes); + std::memset(dest, 0x5a, destBytes); + if (fmask != nullptr) { + target.cmaskAddress = target.dccAddress + ((dccBytes + 4095) & ~std::size_t{4095}); + target.cmaskBytes = CmaskBytes(target.extent.width, target.extent.height); + target.cmaskFastClear = true; + target.fmaskAddress = reinterpret_cast(fmask); + Require(target.cmaskAddress + target.cmaskBytes <= address + importBytes, "(m) the CMASK does not fit the target's import"); + std::memset(reinterpret_cast(target.cmaskAddress), 0xcc, static_cast(target.cmaskBytes)); + std::memset(fmask, 0x11, fmaskBytes); + } + { + GuestAllocations::Mutation mutation; + mutation.Add(block, importBytes, true, true, true); + mutation.Add(dest, destBytes, true, true, true); + if (keys != nullptr) mutation.Add(keys, unit, true, true, true); + if (fmask != nullptr) mutation.Add(fmask, fmaskBytes, true, true, true); + if (kind == Case::ComparedNeighbour) mutation.Add(reinterpret_cast(neighbour), reserved - (importBytes + tail), true, true, true); + } + if (kind == Case::UnwatchedKeys) Unwatch(target.dccAddress, dccBytes); + if (kind == Case::DeferredOff) SetHostImportDeferredSettles(false); + if (kind == Case::ComparedNeighbour) { + const auto* other = HostImportFor(context, neighbour, reserved - (importBytes + tail)); + Require(other != nullptr && other->unwatched && !Watched(neighbour, reserved - (importBytes + tail)), "(m) the import sharing the target's last 64 KiB block was not compared" + describe(other, neighbour, reserved - (importBytes + tail))); + } + if (fmask != nullptr && !fmaskAlone) { + const auto* fmaskImport = HostImportFor(context, target.fmaskAddress, fmaskBytes); + Require(fmaskImport != nullptr && fmaskImport->base == target.fmaskAddress && fmaskImport->bytes == fmaskBytes && fmaskImport->unwatched && !Watched(target.fmaskAddress, fmaskBytes), "(m) the FMASK's import was not compared at first" + describe(fmaskImport, target.fmaskAddress, fmaskBytes)); + } + const auto* import = HostImportFor(context, address, importBytes); + Require(import != nullptr && import->base == address && import->bytes == importBytes && import->unwatched && !Watched(address, importBytes), "(m) the target's import of " + std::to_string(importBytes) + " bytes was not compared at first" + describe(import, address, importBytes)); + HostImportSettleCounts quiet{}; + if (fmaskAlone) { + { + std::lock_guard gpu(GpuMutex()); + const auto start = HostImportSettles(); + Require(WatchedAfterSettle(context, address, importBytes), "(m) the target's import did not settle before the FMASK's import" + describe(ExistingHostImportFor(context, address, importBytes), address, importBytes)); + const auto settledTarget = HostImportSettles(); + Require(settledTarget.submitted > start.submitted && settledTarget.rewatched > start.rewatched, "(m) the target's import returned to the write watch without a settle read"); + } + const std::array, 3> planes{{{address, colorBytes}, {target.dccAddress, dccBytes}, {target.cmaskAddress, static_cast(target.cmaskBytes)}}}; + for (const auto& [at, bytes] : planes) { + const auto* covering = ExistingHostImportFor(context, at, bytes); + Require(Watched(at, bytes) && (covering == nullptr || !covering->unwatched), "(m) a color, DCC or CMASK plane is not watched before the FMASK's import" + describe(covering, at, bytes)); + } + quiet = HostImportSettles(); + const auto* fmaskImport = HostImportFor(context, target.fmaskAddress, fmaskBytes); + Require(fmaskImport != nullptr && fmaskImport->base == target.fmaskAddress && fmaskImport->bytes == fmaskBytes && fmaskImport->unwatched && !fmaskImport->settleOnDemand && !Watched(target.fmaskAddress, fmaskBytes), "(m) the FMASK's import is not compared with its deferred settle wanted" + describe(fmaskImport, target.fmaskAddress, fmaskBytes)); + } + std::unique_lock gpu(GpuMutex()); + if (partial) { + for (int round = 0; round < 20; ++round) { + gpu.unlock(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + gpu.lock(); + HostImportFor(context, address, importBytes); + } + Require(!Watched(address, importBytes), "(m) a deferred settle returned an import that is not a whole number of 64 KiB blocks to the write watch"); + } + const auto before = HostImportSettles(); + Require(!fmaskAlone || before.submitted == quiet.submitted, "(m) a settle read was submitted between the FMASK's import and the target's first use"); + std::string refusal; + try { + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "(m) the multisampled DCC target has no view"); + } catch (const std::exception& error) { + refusal = error.what(); + } + const auto after = HostImportSettles(); + const auto* settled = ExistingHostImportFor(context, address, importBytes); + const char* name = kind == Case::Pending ? "a pending settle" : kind == Case::PartialBlock ? "an import of whole and partial 64 KiB blocks" : kind == Case::ComparedNeighbour ? "a compared import in its last 64 KiB block" : kind == Case::DeferredOff ? "deferred settles off" : kind == Case::UnwatchedKeys ? "explicitly unwatched DCC keys" : kind == Case::PendingFmask ? "an eight-sample target whose color and FMASK imports both have pending settles" : "an eight-sample target whose FMASK alone has a pending settle"; + std::cout << "(m) initial DCC over " << name << " (import " << importBytes << " bytes): " << (refusal.empty() ? "accepted" : refusal) << "; " << after.submitted - before.submitted << " settle reads submitted, " << after.rewatched - before.rewatched << " back in the write watch\n"; + if (kind == Case::ComparedNeighbour || kind == Case::DeferredOff || kind == Case::UnwatchedKeys) { + Require(refusal.find("initial multisampled DCC contents require watched guest memory") != std::string::npos, std::string("(m) an initial multisampled DCC import over ") + name + " was not refused: " + refusal); + Require(kind == Case::UnwatchedKeys || !Watched(address, importBytes), std::string("(m) ") + name + " returned the target's import to the write watch"); + Require(kind != Case::UnwatchedKeys || (Watched(address, importBytes) && !Watched(target.dccAddress, dccBytes)), "(m) the refusal over explicitly unwatched DCC keys did not come from the keys" + describe(settled, address, importBytes)); + return; + } + Require(refusal.empty(), std::string("(m) an initial multisampled DCC import over ") + name + " was refused: " + refusal); + Require(!fmaskAlone || (after.submitted - before.submitted == 1 && after.rewatched - before.rewatched == 1), "(m) the first use over a pending FMASK settle alone submitted " + std::to_string(after.submitted - before.submitted) + " settle reads, not the FMASK's alone"); + Require(after.submitted > before.submitted && after.rewatched > before.rewatched && Watched(address, importBytes) && settled != nullptr && !settled->unwatched, std::string("(m) the initial DCC import over ") + name + " did not settle the import and return it to the write watch"); + if (fmask != nullptr) { + const auto* settledFmask = ExistingHostImportFor(context, target.fmaskAddress, fmaskBytes); + Require(Watched(target.fmaskAddress, fmaskBytes) && settledFmask != nullptr && !settledFmask->unwatched, "(m) the FMASK's import did not settle back into the write watch" + describe(settledFmask, target.fmaskAddress, fmaskBytes)); + } + ColorTarget destination{}; + destination.address = reinterpret_cast(dest); + destination.surfaceAddress = destination.address; + destination.extent = target.extent; + destination.surfaceExtent = target.extent; + destination.format = VK_FORMAT_R8G8B8A8_UNORM; + destination.elementBytes = 4; + destination.bytes = destBytes; + destination.samples = 1; + destination.tileMode = ColorTileMode::RenderTarget; + RunColorMetadataPass(context, {ColorMetadataPass::Mode::Resolve, {destination}, target}); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + std::vector resolved(destBytes / 4); + AgcDriver::GuestMemory::Read(destination.address, std::as_writable_bytes(std::span(resolved))); + Require(std::ranges::all_of(resolved, [](std::uint32_t word) { return word == 0xffffffffu; }), std::string("(m) the target made over ") + name + " did not start from its DCC fast clear"); + BumpCollectEpoch(); + if (fmask != nullptr) { + const auto collectedFmask = CollectWrites(target.fmaskAddress, fmaskBytes); + Require(collectedFmask != 0, "(m) the settled FMASK import is not collected"); + static_cast(fmask)[0] = 0x22; + std::string fmaskReuse; + try { + MultisampleTargetView(context, target); + } catch (const std::exception& error) { + fmaskReuse = error.what(); + } + Require(fmaskReuse.find("writes to a resident native FMASK are not implemented") != std::string::npos, "(m) the resident target missed the CPU store to its FMASK: " + fmaskReuse); + return; + } + const auto collected = CollectWrites(address, importBytes); + Require(collected != 0, "(m) the settled import is not collected"); + static_cast(block)[importBytes - 1] = 0x77; + static_cast(block)[colorBytes] = 0x00; + CollectWritesUncached(address, importBytes); + Require(!UnchangedSince(address + importBytes - 1, 1, collected) && !UnchangedSince(address + colorBytes, 1, collected), "(m) a CPU store after the initial DCC import was missed"); + std::string reuse; + try { + MultisampleTargetView(context, target); + } catch (const std::exception& error) { + reuse = error.what(); + } + Require(reuse.find("writes to a resident multisampled DCC target or its metadata are not implemented") != std::string::npos, "(m) the resident target missed the CPU store to its DCC keys: " + reuse); + }; + run(Case::Pending, 128); + run(Case::PartialBlock, 384); + run(Case::ComparedNeighbour, 384); + run(Case::DeferredOff, 128); + run(Case::UnwatchedKeys, 128); + run(Case::PendingFmask, 128); + run(Case::PendingFmaskAlone, 128); + std::cout << "initial multisampled DCC over a pending settle tests passed\n"; + return true; +} #endif void remappedImportTests(const Device& device) { @@ -3423,7 +3733,7 @@ bool readWrittenStagingTests(const Device& device, Recorder& recorder) { std::cout << "host imports unavailable: read and written staging not tested\n"; return false; } - constexpr std::size_t bytes = 16u << 20u; + constexpr std::size_t bytes = 40u << 20u; #ifdef _WIN32 void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); #else @@ -3456,9 +3766,8 @@ bool readWrittenStagingTests(const Device& device, Recorder& recorder) { Require(boundInPlace(address + 4096, 4u << 20u, false) != context.discreteGpu, context.discreteGpu ? "a written-only 4 MiB element was bound in place on a discrete GPU" : "a written-only 4 MiB element was staged past the written window"); Require(boundInPlace(address + 4096, 12u << 20u, false), "a written-only 12 MiB element was staged past the discrete written window"); Require(!boundInPlace(address + 4096, 4u << 20u, true), "a read and written 4 MiB element was bound in place"); - const auto buildInPlace = [&](std::uint64_t dispatchThreads, bool read, std::uint32_t stride, std::uint32_t extent) { + const auto buildInPlace = [&](std::uint64_t dispatchThreads, bool read, std::uint32_t stride, std::uint32_t extent, std::uint32_t elementBytes = 12u << 20u) { const auto element = address + 4096; - constexpr std::uint32_t elementBytes = 12u << 20u; const std::uint32_t records = stride != 0 ? elementBytes / stride : elementBytes; ShaderRecompiler::RecompileResult program; ShaderRecompiler::DescriptorBinding binding; @@ -3483,7 +3792,12 @@ bool readWrittenStagingTests(const Device& device, Recorder& recorder) { Require(!buildInPlace((12u << 20u) / 16u, true, 16, 0), "a dispatch sweeping a read and written 12 MiB element bound it in place"); Require(buildInPlace(1024, true, 16, 0), "a dispatch touching 16 KiB of a read and written 12 MiB element staged all of it"); Require(buildInPlace(0, true, 16, 0), "a dispatch of unknown size staged a read and written 12 MiB element"); - Require(buildInPlace((12u << 20u) / 16u, false, 16, 0), "a dispatch sweeping a written-only 12 MiB element staged it"); + Require(buildInPlace((12u << 20u) / 32u, false, 32, 0) != context.discreteGpu, context.discreteGpu ? "a dispatch sweeping a written-only 12 MiB element of 32-byte records bound it in place on a discrete GPU" : "a dispatch sweeping a written-only 12 MiB element of 32-byte records staged it on a GPU without separate VRAM"); + Require(buildInPlace((12u << 20u) / 16u, false, 16, 0), "a dispatch sweeping a written-only 12 MiB element of 16-byte records, which its stores write contiguously, staged it"); + Require(buildInPlace(1024, false, 32, 0), "a dispatch touching 32 KiB of a written-only 12 MiB element staged all of it"); + Require(buildInPlace((12u << 20u) / 32u, false, 0, 32), "a dispatch writing a written-only 12 MiB raw element staged it"); + Require(buildInPlace(204800, false, 160, 0, 204800u * 160u) != context.discreteGpu, context.discreteGpu ? "a dispatch sweeping a written-only 31.25 MiB element of 160-byte records bound it in place on a discrete GPU" : "a dispatch sweeping a written-only 31.25 MiB element of 160-byte records staged it on a GPU without separate VRAM"); + Require(buildInPlace(204800, true, 160, 0, 204800u * 160u) != context.discreteGpu, context.discreteGpu ? "a dispatch sweeping a read and written 31.25 MiB element bound it in place on a discrete GPU" : "a dispatch sweeping a read and written 31.25 MiB element staged it past the 16 MiB cap on a GPU without separate VRAM"); Require(!buildInPlace((12u << 20u) / 32u, true, 0, 32), "a dispatch sweeping a read and written 12 MiB raw element bound it in place"); Require(buildInPlace(1024, true, 0, 32), "a dispatch touching 32 KiB of a read and written 12 MiB raw element staged all of it"); Require(buildInPlace((12u << 20u) / 32u, true, 0, 0), "a raw element without an access extent was staged"); @@ -9817,6 +10131,98 @@ bool vertexTargetRangeTests(const Device& device, Recorder& recorder, bool impor return true; } +std::uint64_t processPrivateBytes() { +#ifdef _WIN32 + PROCESS_MEMORY_COUNTERS_EX counters{}; + counters.cb = sizeof(counters); + if (!K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&counters), sizeof(counters))) return 0; + return counters.PrivateUsage; +#else + std::ifstream status("/proc/self/status"); + std::string line; + while (std::getline(status, line)) { + if (line.rfind("RssAnon:", 0) == 0) return std::stoull(line.substr(8)) * 1024u; + } + return 0; +#endif +} + +// What one graphics pipeline the store keeps costs: `count` pipelines, each with its own fragment +// module (so its own fragment shader library), built through CachedPipeline and held, after their +// optimized relinks; process private bytes and VK_EXT_memory_budget heap usage before and after, +// and after the store is cleared. +int pipelineStoreMemoryBench(const Device& device, std::uint32_t count) { + if (!device.Graphics() || count == 0) { + std::cout << "skipped, the pipeline store memory bench requires a graphics queue and at least one pipeline\n"; + return 77; + } + std::lock_guard gpu(GpuMutex()); + const auto& context = device.GetContext(); + struct Cleanup { + VkDevice device; + ~Cleanup() { ClearCachedPipelines(device); } + } cleanup{context.device}; + ClearCachedPipelines(context.device); + ShaderRecompiler::RecompileResult vertex; + vertex.variantId = 21; + vertex.spirv = std::vector(std::begin(TRIANGLE_vert_SPV), std::end(TRIANGLE_vert_SPV)); + std::vector fragments(count + 1u); + for (std::uint32_t index = 0; index <= count; ++index) { + fragments[index].variantId = 1000u + index; + fragments[index].spirv = std::vector(std::begin(TRIANGLE_frag_SPV), std::end(TRIANGLE_frag_SPV)); + fragments[index].spirv[2] = 0x50535430u + index; + } + State state{}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.hasColorTarget = true; + state.color.format = VK_FORMAT_R8G8B8A8_UNORM; + state.colors.push_back(state.color); + state.blend.colorWriteMask = 15; + state.blends.push_back(state.blend); + const std::array first{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &fragments[count], 0}}; + ShaderResources resources(context, first, state.color, static_cast(state.colors.size()), 0, 0); + const VertexInputLayout input; + std::vector> held; + held.reserve(count + 1u); + const auto build = [&](std::uint32_t index) { + const std::array shaders{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &fragments[index], 0}}; + held.push_back(CachedPipeline(context, state, input, resources, shaders, VK_IMAGE_LAYOUT_GENERAL)); + }; + struct Sample { + std::uint64_t privateBytes; + std::optional heaps; + }; + const auto sample = [&] { + WaitForOptimizedPipelines(context.device); + return Sample{processPrivateBytes(), device.MemoryUsage()}; + }; + build(count); + const auto before = sample(); + const auto started = std::chrono::steady_clock::now(); + for (std::uint32_t index = 0; index < count; ++index) build(index); + const auto built = std::chrono::steady_clock::now(); + const auto after = sample(); + const auto linked = std::chrono::steady_clock::now(); + const auto totals = PipelineStoreTotals(); + held.clear(); + ClearCachedPipelines(context.device); + const auto cleared = sample(); + const auto mib = [](std::uint64_t later, std::uint64_t earlier) { return (static_cast(later) - static_cast(earlier)) / (1024.0 * 1024.0); }; + const auto perPipeline = [&](std::uint64_t later, std::uint64_t earlier) { return (static_cast(later) - static_cast(earlier)) / 1024.0 / count; }; + const char* linking = !context.graphicsPipelineLibrary ? "whole pipelines" : context.pipelineLinkTimeOptimization ? "libraries and optimized relinks" : "libraries, fast links only"; + std::cout << std::fixed << std::setprecision(1); + std::cout << "Pipeline store memory: " << count << " graphics pipelines (one fragment module each, " << linking << "), " << totals.cached << " in the store, built in " + << std::chrono::duration(built - started).count() << " s, relinked in " << std::chrono::duration(linked - built).count() << " s\n"; + std::cout << " process private bytes: +" << mib(after.privateBytes, before.privateBytes) << " MiB = " << perPipeline(after.privateBytes, before.privateBytes) << " KiB per pipeline; after clearing the store " << mib(cleared.privateBytes, before.privateBytes) << " MiB above the start\n"; + if (before.heaps && after.heaps && cleared.heaps) { + std::cout << " VK_EXT_memory_budget heap usage: device-local +" << mib(after.heaps->deviceLocal, before.heaps->deviceLocal) << " MiB = " << perPipeline(after.heaps->deviceLocal, before.heaps->deviceLocal) << " KiB per pipeline, host +" << mib(after.heaps->host, before.heaps->host) << " MiB = " << perPipeline(after.heaps->host, before.heaps->host) + << " KiB per pipeline; after clearing the store device-local " << mib(cleared.heaps->deviceLocal, before.heaps->deviceLocal) << " MiB, host " << mib(cleared.heaps->host, before.heaps->host) << " MiB above the start\n"; + } else { + std::cout << " VK_EXT_memory_budget unavailable: no device heap usage\n"; + } + return 0; +} + void pipelineCacheTests(const Device& device, bool benchmark) { std::thread establishThreadedProcess([] {}); establishThreadedProcess.join(); @@ -10652,6 +11058,97 @@ void denormalClearTests(const Device& device, Recorder& recorder) { recorder.Sync(); } +class LibraryCreateFlags { +public: + explicit LibraryCreateFlags(const Context& context) : resolver(context.deviceProc), device(context.device) { + create = function("vkCreateGraphicsPipelines"); + active = this; + } + ~LibraryCreateFlags() { active = nullptr; } + LibraryCreateFlags(const LibraryCreateFlags&) = delete; + LibraryCreateFlags& operator=(const LibraryCreateFlags&) = delete; + PFN_vkGetDeviceProcAddr Resolver() const { return resolve; } + std::uint32_t Libraries() const { return libraries.load(); } + std::uint32_t Retaining() const { return retaining.load(); } + +private: + template + TFunction function(const char* name) const { return reinterpret_cast(resolver(device, name)); } + static VKAPI_ATTR VkResult VKAPI_CALL createPipelines(VkDevice device, VkPipelineCache cache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks* callbacks, VkPipeline* pipelines) { + const auto result = active->create(device, cache, count, infos, callbacks, pipelines); + for (std::uint32_t index = 0; index < count; ++index) { + if (pipelines[index] == VK_NULL_HANDLE || (infos[index].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) == 0) continue; + active->libraries.fetch_add(1); + if ((infos[index].flags & VK_PIPELINE_CREATE_RETAIN_LINK_TIME_OPTIMIZATION_INFO_BIT_EXT) != 0) active->retaining.fetch_add(1); + } + return result; + } + static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL resolve(VkDevice device, const char* name) { + if (std::strcmp(name, "vkCreateGraphicsPipelines") == 0) return reinterpret_cast(createPipelines); + return active->resolver(device, name); + } + PFN_vkGetDeviceProcAddr resolver; + VkDevice device; + PFN_vkCreateGraphicsPipelines create = nullptr; + std::atomic libraries{0}; + std::atomic retaining{0}; + static inline LibraryCreateFlags* active = nullptr; +}; + +void pipelineLibraryLtoInfoTests(const Device& device) { + std::lock_guard gpu(GpuMutex()); + const auto& context = device.GetContext(); + LibraryCreateFlags created(context); + struct Cleanup { + VkDevice device; + ~Cleanup() { ClearCachedPipelines(device); } + } cleanup{context.device}; + ClearCachedPipelines(context.device); + if (!context.graphicsPipelineLibrary) { + std::cout << "graphics pipeline libraries unavailable or disabled: no library retains link-time optimization info\n"; + return; + } + ShaderRecompiler::RecompileResult vertex, fragment; + vertex.variantId = 41; + fragment.variantId = 42; + vertex.spirv = std::vector(std::begin(TRIANGLE_vert_SPV), std::end(TRIANGLE_vert_SPV)); + fragment.spirv = std::vector(std::begin(TRIANGLE_frag_SPV), std::end(TRIANGLE_frag_SPV)); + const std::array shaders{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}; + State state{}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.hasColorTarget = true; + state.color.format = VK_FORMAT_R8G8B8A8_UNORM; + state.colors.push_back(state.color); + state.blend.colorWriteMask = 15; + state.blends.push_back(state.blend); + ShaderResources resources(context, shaders, state.color, static_cast(state.colors.size()), 0, 0); + VertexInputLayout input; + auto fast = context; + fast.deviceProc = created.Resolver(); + fast.pipelineLinkTimeOptimization = false; + auto optimizing = fast; + optimizing.pipelineLinkTimeOptimization = true; + const bool lto = std::getenv("APS5_NO_PIPELINE_LTO") == nullptr; + const auto fastPipeline = CachedPipeline(fast, state, input, resources, shaders, VK_IMAGE_LAYOUT_GENERAL); + const auto fastLibraries = created.Libraries(); + Require(fastLibraries != 0, "the pipeline was not linked from libraries created through the traced device"); + Require(created.Retaining() == 0, std::to_string(created.Retaining()) + " of the " + std::to_string(fastLibraries) + " libraries made for a context without optimized links were created to retain link-time optimization info"); + WaitForOptimizedPipelines(context.device); + Require(!fastPipeline->Optimized(), "a pipeline of a context without optimized links was optimized in the background"); + const auto optimizedPipeline = CachedPipeline(optimizing, state, input, resources, shaders, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + Require(optimizedPipeline != fastPipeline, "the two attachment layouts shared one cached pipeline"); + const auto libraries = created.Libraries() - fastLibraries; + if (lto) { + Require(libraries == fastLibraries, "a link that will be optimized made " + std::to_string(libraries) + " libraries, expected " + std::to_string(fastLibraries) + ": it reused libraries made without link-time optimization info"); + Require(created.Retaining() == libraries, std::to_string(created.Retaining()) + " of the " + std::to_string(libraries) + " libraries of a link that will be optimized were created to retain link-time optimization info"); + } else { + Require(libraries == 0 && created.Retaining() == 0, "with APS5_NO_PIPELINE_LTO set, forcing pipelineLinkTimeOptimization changed the libraries or their create flags"); + } + WaitForOptimizedPipelines(context.device); + Require(optimizedPipeline->Optimized() == lto, lto ? "the pipeline linked from libraries that retain link-time optimization info was not optimized in the background" : "a pipeline was optimized with APS5_NO_PIPELINE_LTO set"); + std::cout << "Pipeline library link-time optimization info tests passed: " << fastLibraries << " libraries without the info, " << created.Retaining() << " with it\n"; +} + void pipelineLibraryTests(const Device& device) { std::lock_guard gpu(GpuMutex()); const auto& context = device.GetContext(); @@ -10971,6 +11468,14 @@ int main(int argc, char** argv) { importWatchTests(device); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--settle-initial-dcc-only") { +#ifdef _WIN32 + return settleInitialDccTests(device) ? 0 : 77; +#else + std::cout << "skipped, the device has no deferred settle of host imports: imports under 1 MiB wait for one only on Windows\n"; + return 77; +#endif + } #ifdef _WIN32 if (argc == 2 && std::string_view(argv[1]) == "--host-import-lifetime-other-family-only") { if (device.GetContext().settleFamily == device.GetContext().queueFamily) { @@ -11024,10 +11529,15 @@ int main(int argc, char** argv) { } return sparseRunImportTests(device, true) ? 0 : 77; } + if (argc == 2 && std::string_view(argv[1]) == "--pipeline-library-lto-info-only") { + pipelineLibraryLtoInfoTests(device); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--pipeline-library-only") { pipelineLibraryTests(device); return 0; } + if ((argc == 2 || argc == 3) && std::string_view(argv[1]) == "--benchmark-pipeline-store-memory") return pipelineStoreMemoryBench(device, argc == 3 ? static_cast(std::stoul(argv[2])) : 4096u); if (argc == 2 && (std::string_view(argv[1]) == "--benchmark-pipeline-cache" || std::string_view(argv[1]) == "--pipeline-cache-only")) { pipelineCacheTests(device, std::string_view(argv[1]) == "--benchmark-pipeline-cache"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp index 8dfde5e87e..202aa559ae 100644 --- a/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp @@ -209,7 +209,8 @@ std::vector Recompiled(std::span code) { request.target.maxWorkgroupSharedMemoryBytes = 49152; request.layout = {0, 0, 0, 128}; request.useCache = false; - const auto& words = Recompile(request).spirv.Words(); + const auto result = Recompile(request); + const auto& words = result.spirv.Words(); return {words.begin(), words.end()}; } diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 4d7e4dc551..2a31594300 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -47,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -406,6 +407,120 @@ void verifyFrontendPair() { } } +void verifySpecializedModuleRelease() { + using namespace ShaderRecompiler; + const std::array color{0x3f800000u}; + const std::vector code{0xbf800003u, 0x7e000200u, 0xf800180fu, 0u, 0xbf810000u}; + auto pixel = FrontendRequest(code, color); + pixel.shader.stage = ShaderStage::Fragment; + pixel.context.compute.reset(); + pixel.context.pixel = ShaderPixelStageInfo{}; + pixel.context.pixel->wave32 = true; + pixel.context.pixel->targetOutputMode[0] = 9u; + pixel.context.waveSize = 32u; + pixel.useCache = true; + SharedSpirv::Weak released; + std::uint64_t specializationId = 0; + std::vector words; + { + const auto first = Recompile(pixel); + require(first.specializationId != 0u, "specialized module release: the pixel fixture was not specialized"); + released = first.spirv.Weaken(); + specializationId = first.specializationId; + words = first.spirv.Copy(); + } + require(released.Expired(), "specialized module release: the module caches kept the words of a module no result uses"); + const auto again = Recompile(pixel); + require(again.specializationId == specializationId, "specialized module release: a module built again from its key got another specialization ID"); + require(std::ranges::equal(again.spirv.Words(), words), "specialized module release: a module built again from its key has other words"); + const auto shared = Recompile(pixel); + require(shared.spirv.data() == again.spirv.data(), "specialized module release: a second result of a live module copied its words"); +} + +const std::vector& resultMemoPixelCode() { + static const std::vector code{0xbf800000u, 0x7e000200u, 0xf800180fu, 0u, 0xbf810000u}; + return code; +} + +bool compileResultMemoPixel(std::uint32_t value) { + using namespace ShaderRecompiler; + std::array color{value}; + auto request = FrontendRequest(resultMemoPixelCode(), color); + request.shader.stage = ShaderStage::Fragment; + request.context.compute.reset(); + request.context.pixel = ShaderPixelStageInfo{}; + request.context.pixel->wave32 = true; + request.context.pixel->targetOutputMode[0] = 9u; + request.context.waveSize = 32u; + request.useCache = true; + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + const auto regions = memory.Regions(); + request.context.memory = regions; + bool hit = false; + static_cast(Recompile(request, *capture, &hit)); + return hit; +} + +void verifyResultMemoBudgetThreads() { + using namespace ShaderRecompiler; + require(!compileResultMemoPixel(1u) && compileResultMemoPixel(1u), "result memo budget threads: the memo did not keep a result"); + const auto one = ResultMemoBytes(); + const auto budget = 3u * one; + SetResultMemoBudget(budget); + static_cast(compileResultMemoPixel(2u)); + static_cast(compileResultMemoPixel(3u)); + require(ResultMemoBytes() == budget, "result memo budget threads: three results did not fill a budget of three"); + std::vector late; + SetResultMemoTrimHook([&] { + const auto before = ResultMemoBytes(); + for (const auto value : {5u, 6u}) late.emplace_back([value] { static_cast(compileResultMemoPixel(value)); }); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30); + while (ResultMemoBytes() < before + 2u * one && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + }); + static_cast(compileResultMemoPixel(4u)); + for (auto& thread : late) thread.join(); + require(late.size() == 2u, "result memo budget threads: the fourth result did not start a trim"); + require(ResultMemoBytes() <= budget, "result memo budget threads: results inserted during a trim left the memo over its budget"); + SetResultMemoBudget(0u); +} + +void verifyResultMemoBudgetMasked() { + using namespace ShaderRecompiler; + require(ResultMemoBytes() == 0u, "result memo budget masked: the memo held results before the first compile"); + require(!compileResultMemoPixel(1u), "result memo budget masked: the first compile hit the memo"); + const auto first = ResultMemoBytes(); + require(!compileResultMemoPixel(2u), "result memo budget masked: a new snapshot hit the memo"); + const auto second = ResultMemoBytes() - first; + require(second != 0u && second < first, std::string("result memo budget masked: the masked table's entry was not counted (first result " + std::to_string(first) + " bytes, patched result " + std::to_string(second) + ")").c_str()); + SetResultMemoBudget(1u); + require(ResultMemoBytes() == 0u, std::string("result memo budget masked: a trim to one byte left " + std::to_string(ResultMemoBytes()) + " bytes").c_str()); + SetResultMemoBudget(0u); + require(!compileResultMemoPixel(3u), "result memo budget masked: a snapshot after the trim hit the memo"); + require(ResultMemoBytes() == first, std::string("result memo budget masked: the trim left a masked entry to patch from (" + std::to_string(ResultMemoBytes()) + " bytes after one compile, " + std::to_string(first) + " for a full one)").c_str()); +} + +void verifyResultMemoBudget() { + using namespace ShaderRecompiler; + const auto compile = [](std::uint32_t value) { return compileResultMemoPixel(value); }; + require(ResultMemoBytes() == 0u, "result memo budget: the memo held results before the first compile"); + require(!compile(1u) && compile(1u), "result memo budget: the memo did not keep a result"); + const auto one = ResultMemoBytes(); + require(one != 0u, "result memo budget: a memo'd result counted no bytes"); + const auto budget = 3u * one; + SetResultMemoBudget(budget); + for (const auto value : {2u, 3u, 4u}) { + require(!compile(value), "result memo budget: a new snapshot hit the memo"); + require(ResultMemoBytes() <= budget, "result memo budget: the memo holds more than its budget"); + } + require(ResultMemoBytes() < budget, "result memo budget: a fourth result over a budget of three was not trimmed"); + require(compile(4u) && compile(3u), "result memo budget: the most recently used results were dropped"); + require(!compile(1u), "result memo budget: the least recently used result outlived the trim"); + SetResultMemoBudget(0u); + for (const auto value : {5u, 6u, 7u}) static_cast(compile(value)); + require(ResultMemoBytes() > budget, "result memo budget: clearing the budget kept bounding the memo"); +} + ShaderRecompiler::RecompileRequest LayoutPairRequest(std::span code, std::span userData, bool registration, std::uint32_t wordLimit) { using namespace ShaderRecompiler; auto request = FrontendRequest(code, userData); @@ -2339,7 +2454,10 @@ void verifyTableSnapshotReuse() { const auto otherBuffer = CompileTable(request); std::copy(outputV.begin(), outputV.end(), guest.srt.begin() + 12); const auto resolvedShared = ResourceMaterializer::TableReuse(); - require(otherBuffer.result != compiled.result && otherBuffer.result->variantId == compiled.result->variantId && resolvedShared.resolutionsShared - resolvedBefore.resolutionsShared == 1u && resolvedShared.resolutions == resolvedBefore.resolutions && TableMap(*otherBuffer.result) == map && TableBinding(*otherBuffer.result, DescriptorRole::ImageTable)->guestDescriptor == images->guestDescriptor, "table snapshot reuse: a result for another buffer did not reuse the unchanged table's resolution"); + const bool resolves = std::getenv("APS5_NO_MASKED_RESULT_MEMO") != nullptr || std::getenv("APS5_VERIFY_MASKED_RESULT_MEMO") != nullptr; + require(otherBuffer.result != compiled.result && otherBuffer.result->variantId == compiled.result->variantId && resolvedShared.resolutionsShared - resolvedBefore.resolutionsShared == (resolves ? 1u : 0u) && resolvedShared.resolutions == resolvedBefore.resolutions && TableMap(*otherBuffer.result) == map && TableBinding(*otherBuffer.result, DescriptorRole::ImageTable)->guestDescriptor == images->guestDescriptor, "table snapshot reuse: a result for another buffer did not reuse the unchanged table's resolution"); + const auto* otherBuffers = TableBinding(*otherBuffer.result, DescriptorRole::GuestBuffers); + require(otherBuffers != nullptr && std::search(otherBuffers->guestDescriptor.begin(), otherBuffers->guestDescriptor.end(), otherOutputV.begin(), otherOutputV.end()) != otherBuffers->guestDescriptor.end(), "table snapshot reuse: a result for another buffer does not hold that buffer's descriptor"); const auto storedWord = heap[5u * 8u]; heap[5u * 8u] = heap[6u * 8u]; const auto restoredMap = map; @@ -3364,6 +3482,19 @@ void verifyImageTableRegistration() { const auto* atomicTable = TableBinding(*atomic.result, DescriptorRole::ImageStorageTable); require((atomicFault & Abi::PoisonFlag) == 0u && atomicTable != nullptr && atomicTable->count != 0u && atomicTable->imageAtomic.size() == atomicTable->count && std::all_of(atomicTable->imageWritten.begin(), atomicTable->imageWritten.end(), [](bool written) { return written; }) && std::all_of(atomicTable->imageAtomic.begin(), atomicTable->imageAtomic.end(), [](bool atomic) { return atomic; }), "image table registration: the image_atomic_add table was not bound as atomic storage elements"); require(TableBinding(*atomic.result, DescriptorRole::ImageTable) == nullptr || TableBinding(*atomic.result, DescriptorRole::ImageTable)->count == 0u, "image table registration: an atomic-only table bound sampled elements"); + auto atomic64Code = storeCode; + atomic64Code[8] = 0xf0440308u; + guest.heap[0] = TableT(texture.bytes.data(), TableFormat32_32UInt, TableType2D); + static constexpr std::array int64ImageCapabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityInt64ImageEXT}; + auto atomic64Request = TableRequest(atomic64Code, userData); + atomic64Request.target.supportedCapabilities = int64ImageCapabilities; + const auto atomic64 = CompileTable(atomic64Request); + const auto& atomic64Plan = *atomic64.capture->plan; + require(atomic64Plan.info.images.size() == 1u && atomic64Plan.info.images[0].table == 0u && atomic64Plan.info.images[0].tableOperation == TableOperation::Supported, "image table registration: a table T# feeding image_atomic64_add was not a supported table operation"); + const auto& atomic64Map = TableMap(*atomic64.result); + const auto atomic64Fault = atomic64Map[Abi::TableHeader(0) + Abi::TableFault]; + const auto* atomic64Table = TableBinding(*atomic64.result, DescriptorRole::ImageStorageTable); + require((atomic64Fault & Abi::PoisonFlag) == 0u && atomic64Table != nullptr && atomic64Table->count != 0u && atomic64Table->imageAtomic64.size() == atomic64Table->count && std::all_of(atomic64Table->imageAtomic64.begin(), atomic64Table->imageAtomic64.end(), [](bool atomic) { return atomic; }), "image table registration: the image_atomic64_add table was not bound as 64-bit atomic storage elements"); const std::array, 2> unsupportedStores{{{0xf0240f08u, "image_store_mip"}, {0xf0280f08u, "image_store_pck"}}}; for (const auto& [instruction, name] : unsupportedStores) { auto code = storeCode; @@ -3794,7 +3925,7 @@ void verifySelectedRecordDirectOperations() { require(word0 == 0xf0440308u || (TableBinding(*compiled.result, DescriptorRole::ImageTable) == nullptr && compiled.result->imageTableFaults == 0u && TableCovered(compiled.regions, &heap[2], sizeof(heap[2]))), (what + " through a T# whose record the CPU evaluates was not bound from its record").c_str()); const auto wholeCode = WithImageOperation(TableWholeCode, word0, false); const auto whole = GetResourcePlan(TableRequest(wholeCode, userData)); - require(whole->info.images.size() == 1u && whole->info.images[0].table == 0u && whole->info.images[0].tableOperation == (word0 == 0xf0200f08u || word0 == 0xf0440108u ? TableOperation::Supported : TableOperation::Unsupported), (what + " through a T# keyed per lane left the image table or has the wrong table operation").c_str()); + require(whole->info.images.size() == 1u && whole->info.images[0].table == 0u && whole->info.images[0].tableOperation == (word0 == 0xf0200f08u || word0 == 0xf0440108u || word0 == 0xf0440308u ? TableOperation::Supported : TableOperation::Unsupported), (what + " through a T# keyed per lane left the image table or has the wrong table operation").c_str()); } const auto mixedCode = WithImageOperation(TableSrtKeyCode, 0xf0200f08u, true); const auto mixed = GetResourcePlan(TableRequest(mixedCode, userData)); @@ -6787,6 +6918,12 @@ void verifyUnnormalizedSamplers() { const auto adjusted = recompile(reservedDword3(0xf0800f09u), userData(normalized)); require(proven(adjusted, false), "sampler dword 3: an image_sample_a whose S# dword 3 holds only a ballot in its reserved bits was not compiled"); expectFailure([&] { static_cast(recompile(reservedDword3(0xf0800f08u), userData(normalized))); }, "GetSamplerResource dword 3 is not a valid runtime value", "sampler dword 3: an image_sample whose S# dword 3 holds a ballot was accepted"); + for (const auto& [adjust, plain, compiled, accepted] : { + std::tuple{0xf1200109u, 0xf1200108u, "sampler dword 3: an image_gather4_c_a whose S# dword 3 holds only a ballot in its reserved bits was not compiled", "sampler dword 3: an image_gather4_c with a computed S# dword 3 was accepted"}, + std::tuple{0xf1600109u, 0xf1600108u, "sampler dword 3: an image_gather4_c_o_a whose S# dword 3 holds only a ballot in its reserved bits was not compiled", "sampler dword 3: an image_gather4_c_o with a computed S# dword 3 was accepted"}}) { + require(proven(recompile(reservedDword3(adjust), imageData(normalized, Type2D, Format32Float, 0u)), false), compiled); + expectFailure([&] { static_cast(recompile(reservedDword3(plain), imageData(normalized, Type2D, Format32Float, 0u))); }, "GetSamplerResource dword 3 is not a valid runtime value", accepted); + } reject(0xf09c0f18u, imageData(unnormalized, TypeCube, Format8888UNorm, 5u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a cube T# was accepted"); reject(0xf09c0f10u, imageData(unnormalized, Type3D, Format8888UNorm, 3u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 3D T# was accepted"); reject(0xf09c0f28u, imageData(unnormalized, Type2DArray, Format8888UNorm, 5u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 2D-array sample was accepted"); @@ -8433,6 +8570,21 @@ int main(int argc, char** argv) { std::cout << "Prepared frontend reuse preserves full and partial-thread programs\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--result-memo-budget") { + verifyResultMemoBudget(); + std::cout << "The result memo keeps its most recently used results within its byte budget\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--result-memo-budget-masked") { + verifyResultMemoBudgetMasked(); + std::cout << "The result memo budget counts and trims the masked table's entries\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--result-memo-budget-threads") { + verifyResultMemoBudgetThreads(); + std::cout << "The result memo ends within its byte budget when results are inserted during a trim\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--layout-pair") { verifyLayoutPair(); std::cout << "Both push-constant layouts of a pixel shader compile from one program unchanged\n"; @@ -8530,6 +8682,7 @@ int main(int argc, char** argv) { verifyTargetFeatureKey(); verifyImageTableKeys(); verifyImageTables(); + verifySpecializedModuleRelease(); verifyImageTableRegistration(); verifyScalarAddressRule(); verifyPointerImageTable(); diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp index 05d848cdf2..956897cb51 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp @@ -460,6 +460,20 @@ void testInternedSpirvSharesEqualWords() { check(edited[6] == spv::CapabilityFloat64 && first[6] == spv::CapabilityShader && again.data() == first.data(), "a write to one holder of interned SPIR-V changed the others"); } +void testInternedSpirvReleasesUnusedWords() { + const std::vector words{spv::MagicNumber, 0x00010400u, 0u, 7u, 0u, (2u << 16u) | spv::OpCapability, spv::CapabilityGeometry}; + SharedSpirv::Weak released; + { + const auto first = InternSpirv(words); + const auto second = InternSpirv(words); + check(first.data() == second.data(), "two equal specialized modules kept separate copies of their words"); + released = first.Weaken(); + } + check(released.Expired(), "the SPIR-V interning table kept words no holder uses"); + const auto again = InternSpirv(words); + check(std::ranges::equal(again.Words(), words) && released.Lock().empty(), "SPIR-V interned again after its release changed its words"); +} + int main() { testFoldingLeavesNoDeadBlocksOrConstants(); testIdsBeyondTheBoundAreRejected(); @@ -477,6 +491,7 @@ int main() { testEmittedWordsAreStable(); testBlockBallotsOfOnePredicateShareOneBallot(); testInternedSpirvSharesEqualWords(); + testInternedSpirvReleasesUnusedWords(); if (failures != 0) { std::fprintf(stderr, "%d check(s) failed\n", failures); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index 3f531ade83..fa67366d65 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -591,8 +591,7 @@ void testSmallCopyBehindPendingSource() { waitFor(&copied, 1, "the label after a small copy behind a pending source never landed"); AgcDriverWaitIdle_nid_postfix(); check(std::equal(destination, destination + smallWords, produced), "a small copy did not see the GPU copy into its source before it"); - if (std::getenv("APS5_COPY_SYNC") != nullptr) check(AgcDriver::DriverDetail::copiesBehindPendingSources.load() == before, "a small copy that has to wait for a completion store over its source went through the GPU copy"); - else check(AgcDriver::DriverDetail::copiesBehindPendingSources.load() == before + 1, "a small copy whose source the GPU still stores into was not recorded on the GPU"); + check(AgcDriver::DriverDetail::copiesBehindPendingSources.load() == before, "a small copy queued behind the pipeline write into its source was recorded inline"); std::fill(destination, destination + smallWords, 0u); const auto settled = AgcDriver::DriverDetail::copiesBehindPendingSources.load(); submit(0, commands(memoryCopy(destination, produced, smallBytes), writeData(&settledCopied, 1))); @@ -953,7 +952,8 @@ int main(int argc, char** argv) { }); for (auto& reader : readers) reader.join(); for (const auto& snapshot : concurrent) check(snapshot == branched, "concurrent raw compute readers lost snapshot reuse"); - alignas(256) std::array, 65> programs{}; + struct alignas(256) RawProgram : std::array {}; + std::vector programs(4097); for (auto& program : programs) program[0] = 0xbf810000; const auto firstAddress = reinterpret_cast(programs.front().data()); readers.clear(); @@ -963,6 +963,9 @@ int main(int argc, char** argv) { for (auto& reader : readers) reader.join(); for (const auto& snapshot : concurrent) check(snapshot == concurrent.front(), "concurrent raw compute misses duplicated snapshots"); const auto evicted = AgcDriver::DriverDetail::ReadRawShader(firstAddress); + for (std::size_t i = 1; i <= 400; ++i) + AgcDriver::DriverDetail::ReadRawShader(reinterpret_cast(programs[i].data())); + check(AgcDriver::DriverDetail::ReadRawShader(firstAddress) == evicted, "raw shader cache evicted a program while a title used only 401"); for (std::size_t i = 1; i < programs.size(); ++i) AgcDriver::DriverDetail::ReadRawShader(reinterpret_cast(programs[i].data())); check(AgcDriver::DriverDetail::ReadRawShader(firstAddress) != evicted && evicted->code[0] == 0xbf810000, diff --git a/core/libs/prx/libSceAgcDriver/tests/TessellationPipelineCache.cpp b/core/libs/prx/libSceAgcDriver/tests/TessellationPipelineCache.cpp new file mode 100644 index 0000000000..6c922e0bc9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/TessellationPipelineCache.cpp @@ -0,0 +1,31 @@ +#include "prx/libSceAgcDriver/Execution/include/TessellationPipelineCache.hpp" +#include + +namespace { + +struct Case { + VkDriverId driver; + bool usable; +}; + +constexpr Case Cases[]{ + {VK_DRIVER_ID_MOLTENVK, false}, + {VK_DRIVER_ID_MESA_RADV, true}, + {VK_DRIVER_ID_MESA_LLVMPIPE, true}, + {VK_DRIVER_ID_NVIDIA_PROPRIETARY, true}, + {VK_DRIVER_ID_AMD_PROPRIETARY, true}, + {VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS, true}, +}; + +} + +int main() { + int failures = 0; + for (const auto& item : Cases) { + if (AgcDriver::TessellationPipelineCacheUsable(item.driver) != item.usable) { + std::fprintf(stderr, "driver %d: expected tessellation pipelines %s the pipeline cache\n", static_cast(item.driver), item.usable ? "to use" : "not to use"); + ++failures; + } + } + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp index 3a13ee2d9e..6bfe87eb6d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -117,6 +117,7 @@ void repackedFormatTests() { Require(IsRepackedTextureFormat(guest) && !IsRepackedTextureFormat(36u) && !IsRepackedTextureFormat(20u), "only texture format 43 is sampled through a repacked copy"); Context context{}; Require(SampledTextureFormat(context, guest) == VK_FORMAT_B10G11R11_UFLOAT_PACK32 && ResolveTextureFormat(guest) == VK_FORMAT_R32_UINT, "format 43 must be sampled as B10G11R11_UFLOAT and stored as R32_UINT"); + Require(SampledTextureFormat(context, 36u) == VK_FORMAT_B10G11R11_UFLOAT_PACK32 && ResolveTextureFormat(36u) == VK_FORMAT_R32_UINT, "format 36 must be sampled as B10G11R11_UFLOAT and stored as R32_UINT"); const auto field = [](std::uint32_t word, VkComponentSwizzle channel) -> std::pair { switch (channel) { case VK_COMPONENT_SWIZZLE_R: return {word & 0x7ffu, 6u}; @@ -217,11 +218,22 @@ void packedFloatDccClearTests() { for (const auto texel : texels) Require(texel == expected, "packed float DCC clear did not encode RGB zero or one"); } } - alignas(64) std::array keys{0xc0, 0xc0, 0xc0, 0xc0}; + Require(DccClearTextureFormat(36u) == VK_FORMAT_B10G11R11_UFLOAT_PACK32 && DccClearTextureFormat(43u) == VK_FORMAT_B10G11R11_UFLOAT_PACK32, "formats 36 and 43 DCC clears must use native packed float layout"); + alignas(64) std::array keys{}; GuestTextureResource resource{}; - resource.format = 36; resource.dccAddress = reinterpret_cast(keys.data()); - Require(TextureClearKeys(resource, 1024) == DccKeys::Clear1111, "packed float texture discarded its white fast-clear keys"); + for (const std::uint32_t format : {36u, 43u}) { + resource.format = format; + keys.fill(0xc0); + Require(TextureClearKeys(resource, 1024) == DccKeys::Clear1111, "packed float texture format " + std::to_string(format) + " discarded its white fast-clear keys"); + keys.fill(0x80); + Require(TextureClearKeys(resource, 1024) == DccKeys::Clear1110, "packed float texture format " + std::to_string(format) + " discarded its 1110 fast-clear keys"); + for (const auto [keysVal, expected] : {std::pair{DccKeys::Clear1111, 0x781e03c0u}, {DccKeys::Clear1110, 0x781e03c0u}, {DccKeys::Clear0000, 0u}}) { + std::array surface{}; + ReadTextureSurface(resource, keysVal, std::as_writable_bytes(std::span(surface))); + for (const auto texel : surface) Require(texel == expected, "packed float format " + std::to_string(format) + " DCC clear surface read produced unexpected texel value"); + } + } } void fillContractTests() { diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp index 48b7f6fa98..fce3aa0322 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp @@ -1,4 +1,6 @@ #include "GraphicsTests.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" @@ -424,26 +426,35 @@ void RunTextureTilingTests() { array.depthOrLastArray = 1536; array.baseArray = 1536; const auto layerBytes = DescribeSurface(array).layerBytes; - const auto window = ViewedArrayWindow(array); + const auto window = ViewedArrayWindow(array, false); Require(window.baseAddress == array.baseAddress + 1536ull * layerBytes && window.depthOrLastArray == 0 && window.baseArray == 0, "a sampled view of one slice at 1536 of an R_X array must start at that slice"); Require(DescribeSurface(window).layerBytes == layerBytes && DescribeSurface(window).layers == 1, "a sampled array window must keep the slice layout"); auto unaligned = array; unaligned.baseArray = 1530; - Require(ViewedArrayWindow(unaligned).baseAddress == array.baseAddress, "an R_X window at a slice the swizzle mixes in must keep the whole array"); + const auto mixed = ViewedArrayWindow(unaligned, false); + Require(mixed.baseAddress == array.baseAddress + 1520ull * layerBytes && mixed.baseArray == 10 && mixed.depthOrLastArray == 16, "an R_X view at a slice the swizzle mixes in must start its window at the last slice before it with those bits clear"); + auto low = array; + low.baseArray = 9; + Require(ViewedArrayWindow(low, false).baseAddress == array.baseAddress, "an R_X view below the first slice with those bits clear keeps the whole array"); auto plain = array; plain.tileMode = TextureTileMode::kStandard64KB; plain.baseArray = 1530; - Require(ViewedArrayWindow(plain).baseAddress == array.baseAddress + 1530ull * DescribeSurface(plain).layerBytes && ViewedArrayWindow(plain).depthOrLastArray == 6, "a non-XOR window may start at any slice"); + Require(ViewedArrayWindow(plain, false).baseAddress == array.baseAddress + 1530ull * DescribeSurface(plain).layerBytes && ViewedArrayWindow(plain, false).depthOrLastArray == 6, "a non-XOR window may start at any slice"); auto fromZero = array; fromZero.baseArray = 0; - Require(ViewedArrayWindow(fromZero).baseAddress == array.baseAddress && ViewedArrayWindow(fromZero).depthOrLastArray == 1536, "a view from slice 0 keeps its descriptor"); + Require(ViewedArrayWindow(fromZero, false).baseAddress == array.baseAddress && ViewedArrayWindow(fromZero, false).depthOrLastArray == 1536, "a view from slice 0 keeps its descriptor"); auto compressed = array; compressed.dccAddress = 0x20000000ull; - Require(ViewedArrayWindow(compressed).baseAddress == array.baseAddress, "a view with DCC metadata keeps its descriptor"); + Require(ViewedArrayWindow(compressed, false).baseAddress == array.baseAddress, "a view with DCC metadata keeps its descriptor"); + const auto stored = ViewedArrayWindow(compressed, true); + Require(stored.baseAddress == window.baseAddress && stored.depthOrLastArray == 0 && stored.baseArray == 0 && stored.dccAddress == 0, "a view whose DCC keys read as stored is cut to its slices without metadata"); + auto compressedFromZero = compressed; + compressedFromZero.baseArray = 0; + Require(ViewedArrayWindow(compressedFromZero, true).baseAddress == array.baseAddress && ViewedArrayWindow(compressedFromZero, true).dccAddress == compressed.dccAddress, "a view with DCC metadata from slice 0 keeps its descriptor"); GuestTextureResource cube{}; cube.baseAddress = 0x30000000ull; @@ -455,13 +466,45 @@ void RunTextureTilingTests() { cube.tileMode = TextureTileMode::kStandard4KB; cube.depthOrLastArray = 7232; cube.baseArray = 7227; - const auto faces = ViewedArrayWindow(cube); + const auto faces = ViewedArrayWindow(cube, false); Require(faces.baseAddress == cube.baseAddress + 7227ull * DescribeSurface(cube).layerBytes && faces.depthOrLastArray == 5 && faces.baseArray == 0 && faces.dimension == TextureDimension::kCube, "a cube view of six faces at 7227 must start at its first face"); + auto compressedVolume = cube; + compressedVolume.dimension = TextureDimension::k3D; + compressedVolume.baseArray = 0; + compressedVolume.dccAddress = 0x20000000ull; + Require(ViewedArrayWindow(compressedVolume, true).dccAddress == compressedVolume.dccAddress, "a volume with DCC metadata keeps its descriptor"); + auto volume = cube; volume.dimension = TextureDimension::k3D; volume.baseArray = 0; - Require(ViewedArrayWindow(volume).baseAddress == cube.baseAddress, "a volume keeps its descriptor"); + Require(ViewedArrayWindow(volume, false).baseAddress == cube.baseAddress, "a volume keeps its descriptor"); + } + + { + const std::array words{0x027c0000u, 0xc1600000u, 0x00ffc0ffu, 0xd1800924u, 0x027cc27cu, 0xc1400000u, 0x00704018u, 0x91b00204u}; + const auto view = DecodeTextureResource(words); + Require(view.baseAddress == 0x27c000000ull && view.dccAddress == 0x91b002040000ull && view.dimension == TextureDimension::k2DArray && view.baseArray == 636 && view.depthOrLastArray == 636 && view.width == 1024 && view.height == 1024 && view.format == 22, "the title's DCC array view decodes to slice 636 of 637"); + const auto whole = DescribeSurface(view); + Require(whole.guestBytes == 2671771648ull, "the uncut DCC array view covers all 637 slices (" + std::to_string(whole.guestBytes) + " bytes)"); + Require(TextureClearKeys(view, whole.guestBytes) == DccKeys::Uncompressed, "unreadable DCC keys of the uncut view read the texels as stored"); + Require(ViewedArrayWindow(view, false).baseAddress == view.baseAddress, "a DCC view whose keys are not known to read as stored keeps its descriptor"); + const auto window = ViewedArrayWindow(view, true); + const auto cut = DescribeSurface(window); + Require(window.baseAddress == view.baseAddress + whole.GuestLayerOffset(624) && window.dccAddress == 0 && window.baseArray == 12 && window.depthOrLastArray == 12, "the window of a stored-keys DCC view starts at the slice-swizzle boundary below its slice without metadata"); + Require(cut.layers == 13 && cut.layerBytes == whole.layerBytes && window.baseAddress + cut.guestBytes == view.baseAddress + whole.guestBytes, "the window covers slices 624..636 (" + std::to_string(cut.guestBytes) + " bytes)"); + for (std::uint32_t slice = 0; slice < cut.layers; ++slice) Require(window.baseAddress + cut.GuestLayerOffset(slice) == view.baseAddress + whole.GuestLayerOffset(624 + slice), "a window slice lies where the uncut surface keeps it"); + const auto* equation = FindTextureSwizzleEquation(XorSwizzleMode(view.tileMode), 4); + const auto block = ThinBlockLayout(view.tileMode, 4); + Require(equation != nullptr && whole.mips.size() == 1 && !whole.mips[0].tail, "the title's DCC array is one untailed Z_X level"); + const auto texel = [&](const GuestTextureResource& surface, const SurfaceGeometry& geometry, std::uint32_t x, std::uint32_t y, std::uint32_t slice) { + const auto& mip = geometry.mips[0]; + return surface.baseAddress + geometry.GuestLayerOffset(slice) + mip.tiledOffset + (static_cast(y / block[2]) * mip.blocksPerRow + x / block[1]) * block[0] + equationOffset(*equation, x, y, slice); + }; + for (std::uint32_t y = 0; y < 1024; y += 7) { + for (std::uint32_t x = 0; x < 1024; x += 5) Require(texel(window, cut, x, y, window.baseArray) == texel(view, whole, x, y, view.baseArray), "a texel of the viewed slice detiles from the same guest byte in the window as in the uncut surface"); + } + Require(TextureClearKeys(window, cut.guestBytes) == DccKeys::Uncompressed, "the window reads its texels as stored, as the uncut view does"); } { diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index af8efbc48e..ddc92eaab0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -36,6 +36,7 @@ constexpr std::size_t Block = 65536; #ifdef _WIN32 constexpr wchar_t HostHoleMode[] = L"--host-hole"; constexpr wchar_t ResidentRemapMode[] = L"--resident-direct-remap"; +constexpr wchar_t RewatchImportPagesMode[] = L"--rewatch-import-pages"; constexpr wchar_t RewatchedSharedMode[] = L"--rewatched-direct-shared"; constexpr wchar_t ImportAliasSharedMode[] = L"--import-alias-shared"; constexpr wchar_t SharedGenerationsMode[] = L"--compared-shared-generations"; @@ -712,13 +713,18 @@ void CheckRewatch() { const auto unwatched = UnwatchSerial(); Require(!Watched(imported, Block) && UntrackedHint(imported, Block) != 0, "the unwatched block does not read as untracked"); bytes[Block + 8] = 0x22; + const std::array, 1> unaligned{{{imported + 1, imported + Block}}}; + Rewatch(unaligned); + Require(!Watched(imported + 4096, Block - 4096) && UnwatchSerial() == unwatched, "a range that is not whole pages returned to the write watch"); const std::array, 1> partial{{{imported + 4096, imported + Block}}}; Rewatch(partial); - Require(!Watched(imported, Block) && UnwatchSerial() == unwatched, "part of a block returned to the write watch"); + Require(Watched(imported + 4096, Block - 4096) && !Watched(imported, 4096) && !Watched(imported, Block), "rewatching part of a block did not return exactly its pages to the write watch"); + const auto partly = UnwatchSerial(); + Require(partly != unwatched, "rewatching part of a block did not invalidate what was cached while it was unwatched"); const std::array, 1> first{{{imported, imported + Block}}}; Rewatch(first); Require(Watched(imported, Block) && !Watched(imported + Block, Block), "rewatching did not return exactly its block to the write watch"); - Require(UnwatchSerial() != unwatched, "rewatching did not invalidate what was cached while the block was unwatched"); + Require(UnwatchSerial() != partly, "rewatching did not invalidate what was cached while the block was unwatched"); Require(UntrackedHint(imported, Block) == 0, "a rewatched block still reads as untracked"); Require(!UnchangedSince(imported, Block, before) && StoredOver(imported, Block, before), "a block returned to the write watch reads as unchanged since before it left"); Require(!UnchangedSinceCollected(imported, Block, before), "a CPU store made while the block was unwatched reads as no CPU store"); @@ -740,6 +746,125 @@ void CheckRewatch() { GuestArena::GuestArenaRelease_nid_postfix(memory, 4 * Block); } +using RewatchSpan = std::array, 1>; + +RewatchSpan BlockSpan(std::uint64_t address, std::size_t bytes) { + return {{{address & ~std::uint64_t{Block - 1}, (address + bytes + Block - 1) & ~std::uint64_t{Block - 1}}}}; +} + +RewatchSpan PageSpan(std::uint64_t address, std::size_t bytes) { + return {{{address & ~std::uint64_t{4095}, (address + bytes + 4095) & ~std::uint64_t{4095}}}}; +} + +void PrintPageState(const char* page, const void* address) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(address, &info, sizeof(info)) == sizeof(info), "cannot query a page next to an import"); + std::cout << page << ": State 0x" << std::hex << info.State << " Type 0x" << info.Type << " Protect 0x" << info.Protect << std::dec << '\n'; +} + +struct ImportFixture { + std::uint8_t* memory; + std::size_t reserved; + ImportFixture(std::size_t reservedBytes, std::size_t committed) : memory(static_cast(GuestArena::GuestArenaAllocate_nid_postfix(reservedBytes, Block))), reserved(reservedBytes) { + Require(memory != nullptr, "cannot reserve an import fixture"); + GuestArena::GuestArenaCommit_nid_postfix(memory, committed, PAGE_READWRITE, committed); + std::memset(memory, 0x11, committed); + BumpCollectEpoch(); + Require(CollectWrites(Base(), committed) != 0, "an import fixture was not collected"); + } + ImportFixture(const ImportFixture&) = delete; + ImportFixture& operator=(const ImportFixture&) = delete; + ~ImportFixture() { + GuestArena::GuestArenaReset_nid_postfix(memory, reserved); + GuestArena::GuestArenaRelease_nid_postfix(memory, reserved); + } + std::uint64_t Base() const { + return reinterpret_cast(memory); + } +}; + +void UnwatchImport(std::uint64_t address, std::size_t bytes) { + Require(ImportWatched(address, bytes, [] { return true; }, false) && !Watched(address, bytes), "an import that leaves the watch stayed watched"); +} + +void RequireRewatchedStores(std::uint64_t address, std::size_t bytes) { + BumpCollectEpoch(); + const auto collected = CollectWrites(address, bytes); + Require(collected != 0 && UnchangedSince(address, bytes, collected), "a rewatched import is not collected"); + *reinterpret_cast(address + bytes - 1) = 0x22; + CollectWritesUncached(address, bytes); + Require(!UnchangedSince(address + bytes - 1, 1, collected), "a CPU store into a rewatched import's last page was missed"); +} + +void CheckRewatchWholeBlockImport() { + constexpr std::size_t bytes = 4 * Block; + ImportFixture fixture(6 * Block, 6 * Block); + const auto import = fixture.Base() + Block; + const auto before = CollectWrites(fixture.Base(), 6 * Block); + UnwatchImport(import, bytes); + Require(PageSpan(import, bytes) == BlockSpan(import, bytes), "a whole-block import has a page span unlike its block span"); + Rewatch(PageSpan(import, bytes)); + Require(Watched(import, bytes), "a settled whole-block import did not return to the write watch"); + Require(!UnchangedSince(import, bytes, before) && UnchangedSince(fixture.Base(), Block, before) && UnchangedSince(import + bytes, Block, before), "rewatching a whole-block import did not stamp exactly its blocks"); + RequireRewatchedStores(import, bytes); +} + +void CheckRewatchImportTail() { + constexpr std::size_t bytes = 0xc4000; + ImportFixture fixture(13 * Block, bytes); + const auto import = fixture.Base(); + UnwatchImport(import, bytes); + PrintPageState("page past a 0xc4000-byte import", fixture.memory + bytes); + const auto unwatched = UnwatchSerial(); + Rewatch(BlockSpan(import, bytes)); + Require(!Watched(import, bytes) && UnwatchSerial() == unwatched, "the 64 KiB span past the import's committed pages returned to the write watch"); + Rewatch(PageSpan(import, bytes)); + Require(Watched(import, bytes), "a settled import whose size is not a whole block did not return to the write watch"); + Require(Watched(import + 12 * Block, bytes - 12 * Block) && !Watched(import, bytes + 1) && !Watched(import + bytes - 1, 2) && !Watched(import + bytes, 4096), "rewatching an import ending inside a block did not return exactly its pages"); + Require(UnwatchSerial() != unwatched && UntrackedHint(import, bytes) == 0, "a rewatched import still reads as unwatched"); + RequireRewatchedStores(import, bytes); +} + +void CheckRewatchUnalignedImport() { + constexpr std::size_t offset = 0x1000; + constexpr std::size_t bytes = 0xc4000; + constexpr std::size_t committed = 0xc8000; + ImportFixture fixture(13 * Block, committed); + const auto import = fixture.Base() + offset; + UnwatchImport(import, bytes); + PrintPageState("page past an import at block offset 0x1000", fixture.memory + committed); + Rewatch(BlockSpan(import, bytes)); + Require(!Watched(import, bytes), "the 64 KiB span past the committed pages around an import returned to the write watch"); + Rewatch(PageSpan(import, bytes)); + Require(Watched(import, bytes), "a settled import that does not start on a block did not return to the write watch"); + Require(!Watched(fixture.Base(), offset) && !Watched(import + bytes, 4096), "rewatching an import returned pages outside it"); + RequireRewatchedStores(import, bytes); +} + +void CheckRewatchImportBesideUnwatchedImport() { + constexpr std::size_t bytes = 0xc4000; + constexpr std::size_t otherBytes = 0x1c000; + ImportFixture fixture(14 * Block, 14 * Block); + const auto import = fixture.Base(); + const auto other = import + bytes; + UnwatchImport(import, bytes); + UnwatchImport(other, otherBytes); + Rewatch(PageSpan(import, bytes)); + Require(Watched(import, bytes), "an import sharing its last block with another unwatched import did not return to the write watch"); + Require(!Watched(other, 4096) && !Watched(import + 13 * Block - 4096, 4096) && !Watched(other + otherBytes - 4096, 4096), "rewatching an import returned another unwatched import's pages in its last block"); + Require(UntrackedHint(other, otherBytes) != 0, "an unwatched import beside a rewatched one does not read as untracked"); + RequireRewatchedStores(import, bytes); + Rewatch(PageSpan(other, otherBytes)); + Require(Watched(import, bytes + otherBytes), "rewatching the second import did not return both to the write watch"); +} + +void CheckRewatchImportPages() { + CheckRewatchWholeBlockImport(); + CheckRewatchImportTail(); + CheckRewatchUnalignedImport(); + CheckRewatchImportBesideUnwatchedImport(); +} + void CheckRewatchedDirectMemoryComparedWhenShared() { constexpr std::size_t bytes = 2 * Block; auto* memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(2 * bytes, Block)); @@ -1381,7 +1506,7 @@ void CheckImportWindow() { bytes[0x100] = 0x11; bytes[Block + 0x2000] = 0x22; return true; - }), "the import window import failed"); + }, true, false), "the import window import failed"); Require(Watched(base, 3 * Block), "an import window left its range unwatched"); const std::array generations{synced, synced, synced}; std::array changed{}; @@ -1593,6 +1718,80 @@ void CheckUnwatchedImport() { #endif } +struct HookCall { + std::uint64_t address; + std::size_t bytes; + bool watched; +}; + +std::vector hookCalls; +std::uint64_t hookProbe = 0; +std::size_t hookProbeBytes = 0; + +void RecordHook(std::uint64_t address, std::size_t bytes) { + hookCalls.push_back({address, bytes, Watched(hookProbe, hookProbeBytes)}); +} + +struct RecordedHook { + RecordedHook() { + hookCalls.clear(); + SetUnwatchHook(&RecordHook); + } + ~RecordedHook() { SetUnwatchHook(nullptr); } + RecordedHook(const RecordedHook&) = delete; + RecordedHook& operator=(const RecordedHook&) = delete; +}; + +void RequireHookOver(std::uint64_t address, std::size_t bytes, const std::string& path) { + Require(hookCalls.size() == 1, path + " did not run the unwatch hook once"); + const auto& call = hookCalls.front(); + Require(call.address <= address && address + bytes <= call.address + call.bytes, path + " gave the unwatch hook a range that misses its own"); + Require(call.watched, path + " ran the unwatch hook after its range left the write watch"); +} + +void CheckUnwatchHooks() { + void* memory = AllocateWatched(8 * Block); + const auto base = reinterpret_cast(memory); + std::memset(memory, 0x11, 8 * Block); + Require(CollectWritesUncached(base, 8 * Block) != 0, "the hooked block is not collected"); + const RecordedHook recorded; + hookProbe = base; + hookProbeBytes = Block; + Unwatch(base, Block); + RequireHookOver(base, Block, "Unwatch"); + Require(!Watched(base, Block), "Unwatch left its range watched"); + + hookCalls.clear(); + hookProbe = base + Block; + bool hookedBeforeImport = false; + Require(ImportWatched(base + Block, Block, [&] { + hookedBeforeImport = hookCalls.size() == 1; + return true; + }, false), "an import that does not stay watched failed"); + RequireHookOver(base + Block, Block, "an import that does not stay watched"); + Require(hookedBeforeImport, "an import that does not stay watched ran the unwatch hook after the import"); + Require(!Watched(base + Block, Block), "an import that does not stay watched left its range watched"); + + hookCalls.clear(); + Require(ImportWatched(base + 2 * Block, Block, [] { return true; }), "an import that stays watched failed"); + Require(hookCalls.empty() && Watched(base + 2 * Block, Block), "an import whose page walk vouched for its pages ran the unwatch hook or gave up its range"); + +#ifndef _WIN32 + hookCalls.clear(); + const auto late = base + 4 * Block; + hookProbe = late; + bool hookedBeforeLoss = false; + Require(ImportWatched(late, 3 * Block, [&] { + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(reinterpret_cast(late + 2 * Block), Block); + hookedBeforeLoss = !hookCalls.empty(); + return true; + }), "an import whose range lost a block during the import failed"); + RequireHookOver(late, 3 * Block, "an import whose page walk failed after the import"); + Require(!hookedBeforeLoss, "an import whose page walk failed after the import ran the unwatch hook before it"); + Require(!Watched(late, Block), "an import whose page walk failed after the import left its range watched"); +#endif +} + class PartlyCommitted { public: static constexpr std::size_t Bytes = 4 * Block; @@ -1773,6 +1972,10 @@ int main() { CheckResidentDirectMappingReuse(); return 0; } + if (std::wstring_view(GetCommandLineW()).ends_with(RewatchImportPagesMode)) { + CheckRewatchImportPages(); + return 0; + } if (std::wstring_view(GetCommandLineW()).ends_with(RewatchedSharedMode)) { CheckRewatchedDirectMemoryComparedWhenShared(); CheckImportedDirectMemoryComparedWhenShared(); @@ -1819,6 +2022,7 @@ int main() { CheckStoreAcrossImports(); CheckImportWindowFootprint(); CheckUnwatchedImport(); + CheckUnwatchHooks(); CheckCommittedRanges(); CheckAdoptLatestEpoch(); } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp index 62e56cfe52..121205b1a2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp @@ -99,6 +99,17 @@ int main() { coverage.Exclude(base + block, block, 43); require(coverage.Restore(base + block - page, 2 * page, 42), "a newer boundary page was not restored"); require(coverage.Covers(base + block - page, page) && !coverage.Covers(base + block, page), "a stale boundary mapping erased a newer neighbouring exclusion"); + constexpr std::size_t tail = 0xc4000; + coverage.Initialize(base, 14 * block); + coverage.Exclude(base, tail, 50); + require(coverage.Restore(base, tail, 51), "an import ending inside a block did not restore its pages"); + require(coverage.Covers(base, tail) && coverage.Covers(base + 12 * block, tail - 12 * block), "an import ending inside a block is not watched"); + require(!coverage.Covers(base, tail + 1) && !coverage.Covers(base + tail - 1, 2) && !coverage.Covers(base + tail, page) && !coverage.Covers(base, 13 * block), "a range past an import ending inside a block is watched"); + require(coverage.Covers(base + tail - page, page) && !coverage.Covers(base + 13 * block - page, page) && coverage.Covers(base + 13 * block, block), "an import ending inside a block changed the pages after it"); + coverage.Initialize(base, 14 * block); + coverage.Exclude(base + page, tail, 60); + require(coverage.Restore(base + page, tail, 61) && coverage.Covers(base + page, tail), "an import starting and ending inside blocks is not watched"); + require(!coverage.Covers(base, page) && !coverage.Covers(base, tail) && !coverage.Covers(base + page + tail, page) && !coverage.Covers(base + page, tail + 1), "an import starting and ending inside blocks watched pages outside it"); std::cout << "write watch coverage passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp index 9278d67787..bc4e3ffab8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp @@ -141,6 +141,7 @@ struct Step { const std::uint32_t* pixel; std::uint64_t pointer; std::array descriptor{}; + std::uint64_t color = 0; }; void Submit(const GuestBlock& vertices, const GuestBlock& target, std::initializer_list steps, bool linear = false) { @@ -179,6 +180,10 @@ void Submit(const GuestBlock& vertices, const GuestBlock& target, std::initializ Append(commands, 0x76, {0x008, static_cast(pixel >> 8u), static_cast(pixel >> 40u)}); Append(commands, 0x76, {0x00e, static_cast(step.pointer), static_cast(step.pointer >> 32u)}); Append(commands, 0x76, {0x010, step.descriptor[0], step.descriptor[1], step.descriptor[2], step.descriptor[3]}); + if (step.color != 0) { + Append(commands, 0x69, {0x318, static_cast(step.color >> 8u)}); + Append(commands, 0x69, {0x390, static_cast(step.color >> 40u)}); + } Append(commands, 0x2d, {3, 2}); } Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; @@ -285,6 +290,26 @@ void TargetReadAsWhenSplit(const GuestBlock& vertices, const GuestBlock& target, if (!linear && !split) Expect(after.passesContinued - before.passesContinued == 1, what + ": the reading draw did not continue the rendering draw's pass, so the case does not test it: " + Counts(before, after)); } +void TargetReadSynchronously(const GuestBlock& vertices, const GuestBlock& target, const GuestBlock& unmapped) { + Submit(vertices, target, {{PixelRead.data(), target.At(ColorOffset)}}, true); + std::memset(target.Bytes(ColorOffset), 0, ColorBytes); + std::memset(unmapped.Bytes(ColorOffset), 0, ColorBytes); + const auto before = AddressDraws(); + const auto otherBefore = OtherDraws(); + Submit(vertices, target, {{PixelColorWord.data(), 0}, {PixelRead.data(), target.At(ColorOffset), {}, unmapped.At(ColorOffset)}}, true); + const auto after = AddressDraws(); + const auto otherAfter = OtherDraws(); + const unsigned red = unmapped.Bytes(ColorOffset)[0]; + std::printf("a synchronous draw reading through the table the linear target the previous draw rendered read %s (red byte %u); recorded: %s, %s\n", red == 255u ? "that draw's color" : "the bytes before it", red, Counts(before, after).c_str(), Counts(otherBefore, otherAfter, "other draws").c_str()); + if (otherAfter.draws == otherBefore.draws) { + std::puts("a synchronous draw reading through the table the linear target the previous draw rendered: skipped, that draw was not recorded because the linear target is not resident on this device, so the target was stored right after it"); + return; + } + Expect(otherAfter.draws - otherBefore.draws == 1, "the draw rendering the linear target was not recorded as one draw: " + Counts(otherBefore, otherAfter, "other draws")); + Expect(after.draws == before.draws, "the draw reading through the table into an unregistered target was recorded, so the case does not test a synchronous draw: " + Counts(before, after)); + Expect(red == 255u, "a synchronous draw reading through the table the linear target the previous draw rendered read the bytes before it (red byte " + std::to_string(red) + "), not that draw's color"); +} + } int main(int argc, char** argv) { @@ -306,6 +331,7 @@ int main(int argc, char** argv) { GuestBlock readOnlyVertices; GuestBlock data; GuestBlock target; + GuestBlock unmapped; FillVertices(readOnlyVertices); readOnlyVertices.Map(false); data.Map(true); @@ -320,6 +346,7 @@ int main(int argc, char** argv) { TargetReadAsWhenSplit(readOnlyVertices, target, false); TargetReadAsWhenSplit(readOnlyVertices, target, true); } + if (only.empty() || only == "--synchronous-target-read") TargetReadSynchronously(readOnlyVertices, target, unmapped); AgcDriverShutdown_nid_postfix(); std::puts("address passes: draws reading through the BDA table share a render pass, and a store through it still begins one"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp index 5c662f4b2d..8146f69adb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp @@ -808,11 +808,12 @@ void DccRefillTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSampl }; record(metadata, dccBytes, 0xc0c0c0c0u); if (cmask != nullptr) record(*cmask, target.cmaskBytes, 0xccccccccu); - if (!AgcDriver::GuestMemory::Watched(metadata.Address(), dccBytes) || (cmask != nullptr && !AgcDriver::GuestMemory::Watched(cmask->Address(), target.cmaskBytes))) expected = "requires watched guest memory"; + const bool unwatched = !AgcDriver::GuestMemory::Watched(metadata.Address(), dccBytes) || (cmask != nullptr && !AgcDriver::GuestMemory::Watched(cmask->Address(), target.cmaskBytes)); #ifdef _WIN32 - rewatchable = !expected.empty(); + if (unwatched) expected = "requires watched guest memory"; + rewatchable = unwatched; #else - Require(mode == 6 || expected.empty(), "a GPU fill of multisampled DCC metadata left it unwatched although host imports stay watched"); + Require(unwatched == (mode == 6), "a GPU fill of multisampled DCC metadata did not take the write-watch mode of its host import"); #endif } else if (mode == 2) { fill(metadata, dccBytes / 2u, 0xc0c0c0c0u); @@ -914,6 +915,170 @@ void DccImportWindowTests(AgcDriver::VulkanDevice& device, std::uint32_t selecte } } +#ifndef _WIN32 +const char* DccComparedImportTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSamples) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if (context.hostImportAlignment == 0) return "skipped, the device imports no host memory, so no multisampled DCC plane lies in a compared host import"; + Require(AgcDriver::GuestMemory::WriteWatched(), "the guest arena is not write-watched"); + const auto decided = AgcDriver::Graphics::PrepareImportWatch(context); + if (decided != AgcDriver::Graphics::ImportWatch::Unwatch) return "skipped, the device reports host imports that keep write protection, so the write-watch probe compares none of them"; + struct Restore { + const AgcDriver::Graphics::Context& context; + AgcDriver::Graphics::ImportWatch decided; + ~Restore() { AgcDriver::Graphics::SetImportWatch(context, decided); } + } restore{context, decided}; + const auto probe = AgcDriver::Graphics::ProbeImportWriteProtection(context); + const bool watchedImportsHold = probe.failure == nullptr && probe.writtenAtImport == 0 && probe.writtenAfterSubmit == 0; + if (!watchedImportsHold) std::cout << "the watched-import control case is skipped: the device reports watched host memory written when it imports or reads it\n"; + const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 8); + std::vector> blocks; + struct ClearTargets { + AgcDriver::VulkanDevice& device; + ~ClearTargets() { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + device.WaitIdle(); + AgcDriver::Graphics::ClearMultisampleTargets(device.Device()); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); + } + } clearTargets{device}; + const auto expectRange = [](std::string_view message, const char* plane, std::uint64_t address, std::size_t bytes, std::uint32_t samples) { + char detail[160]; + std::snprintf(detail, sizeof(detail), "(%s range 0x%llx+0x%zx, %u samples)", plane, static_cast(address), bytes, samples); + Require(message.find(detail) != std::string_view::npos, "a refusal over a compared host import did not identify the affected plane, range and sample count"); + }; + const Color written{0.25f, 0.25f, 0.25f, 0.25f}; + const Color initial{0.0f, 0.0f, 0.0f, 1.0f}; + constexpr std::string_view deviceLimit = "AGC graphics: the device cannot render color with the target's sample count"; + for (const auto samples : {2u, 4u, 8u}) { + if (selectedSamples != 0 && samples != selectedSamples) continue; + for (const auto mode : {7u, 8u, 9u, 10u, 11u, 0u, 1u, 2u, 3u, 4u, 5u, 6u}) { + if (mode >= 7 && samples != 8) continue; + if (mode == 5 && !watchedImportsHold) continue; + const bool watchedImports = mode == 5; + AgcDriver::Graphics::SetImportWatch(context, watchedImports ? AgcDriver::Graphics::ImportWatch::Watch : decided); + blocks.push_back(std::make_unique(layout.Bytes() * samples)); + auto& source = *blocks.back(); + std::memset(source.data, 0, source.bytes); + blocks.push_back(std::make_unique(BlockBytes)); + auto& metadata = *blocks.back(); + std::memset(metadata.data, 0x40, metadata.bytes); + blocks.push_back(std::make_unique(BlockBytes)); + auto& cmask = *blocks.back(); + std::memset(cmask.data, 0xcc, cmask.bytes); + auto target = DecodeTarget(source, samples); + target.dccAddress = metadata.Address(); + target.dccPipeAligned = true; + target.dccAlphaOnMsb = true; + target.cmaskAddress = cmask.Address(); + target.cmaskBytes = AgcDriver::Graphics::CmaskBytes(Width, Height); + target.cmaskFastClear = true; + Block* fmask = nullptr; + if (mode >= 7) { + blocks.push_back(std::make_unique(2u * BlockBytes)); + fmask = blocks.back().get(); + std::memset(fmask->data, 0x11, fmask->bytes); + target.fmaskAddress = fmask->Address(); + target.fmaskPipeBankXor = 0; + } + const std::array, 3> planes{{{&source, static_cast(AgcDriver::Graphics::MultisampledColorBytes(target))}, {&metadata, AgcDriver::Graphics::MultisampledDccBytes(target)}, {&cmask, static_cast(target.cmaskBytes)}}}; + if (mode == 4) { + AgcDriver::GuestMemory::Unwatch(source.Address(), source.bytes); + } else if (mode == 11) { + AgcDriver::GuestMemory::Unwatch(fmask->Address(), fmask->bytes); + } else { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + for (std::size_t plane = 0; plane < planes.size(); ++plane) { + if ((mode == 6 && plane != 1) || mode == 8) continue; + const auto* import = AgcDriver::Graphics::HostImportFor(context, planes[plane].first->Address(), planes[plane].first->bytes); + Require(import != nullptr, "the host import of a multisampled DCC plane failed"); + Require(import->unwatched != watchedImports && AgcDriver::GuestMemory::Watched(planes[plane].first->Address(), planes[plane].second) == watchedImports, "a host import of a multisampled DCC plane did not take the write-watch mode the test set"); + } + if (fmask != nullptr) { + const auto* import = AgcDriver::Graphics::HostImportFor(context, fmask->Address(), fmask->bytes); + Require(import != nullptr && import->unwatched && !AgcDriver::GuestMemory::Watched(fmask->Address(), fmask->bytes), "the host import of the FMASK plane was not compared"); + } + if (mode == 8) { + for (const auto& [block, bytes] : planes) Require(AgcDriver::GuestMemory::Watched(block->Address(), bytes), "the FMASK plane's import took a color or metadata plane out of the write watch"); + } + } + const char* fmaskCase = mode == 7 ? "color, DCC, CMASK and FMASK compared" : mode == 8 ? "only the FMASK compared" : mode == 9 ? "a CPU store of a new byte into a compared FMASK" : mode == 10 ? "a same-value driver store into a compared FMASK" : "an explicitly unwatched FMASK"; + const std::string_view refusal = mode == 4 ? "initial multisampled DCC contents require watched" : mode == 11 ? "native FMASK contents require watched guest memory" : ""; + bool refused = false; + try { + device.ColorMetadataPass({AgcDriver::Graphics::ColorMetadataPass::Mode::FmaskDecompress, {target}}); + Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + } catch (const std::runtime_error& error) { + if (mode >= 7 && mode != 11 && std::string_view(error.what()) == deviceLimit) { + std::printf("8 samples, %s: the FMASK passed the initial import checks, then %s, so its reuse is not tested\n", fmaskCase, error.what()); + programCount = 0; + continue; + } + if (refusal.empty()) throw; + refused = std::string_view(error.what()).find(refusal) != std::string_view::npos; + } + if (!refusal.empty()) { + Require(refused, mode == 11 ? "an explicitly unwatched FMASK range was not refused at its initial import" : "an explicitly unwatched multisampled DCC color range was not refused at its initial import"); + if (mode == 11) std::printf("8 samples, %s: refused at the initial import\n", fmaskCase); + programCount = 0; + continue; + } + blocks.push_back(std::make_unique(layout.Bytes())); + auto& resolved = *blocks.back(); + AgcDriver::Graphics::ColorMetadataPass pass{}; + pass.mode = AgcDriver::Graphics::ColorMetadataPass::Mode::Resolve; + pass.source = target; + pass.targets = {DecodeTarget(resolved)}; + const auto resolve = [&] { + std::memset(resolved.data, 0, resolved.bytes); + 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) { + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto expectedValue = x < ScissorWidth ? initial[channel] + (written[channel] - initial[channel]) / static_cast(samples) : initial[channel]; + Require(FromHalf(stored[layout.Offset(x, y) / 2u + channel]) == expectedValue, "a multisampled DCC target over a compared host import did not start from the fast clear its metadata holds"); + } + } + } + }; + resolve(); + if (mode == 0 || (mode >= 5 && mode <= 8)) { + Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + resolve(); + if (mode >= 7) std::printf("8 samples, %s: imported and reused\n", fmaskCase); + programCount = 0; + continue; + } + const char* plane = mode == 1 ? "color" : mode == 2 ? "DCC" : mode == 3 ? "CMASK" : "FMASK"; + const auto& [block, bytes] = mode >= 9 ? std::pair{fmask, fmask->bytes} : planes[mode - 1u]; + if (mode == 1) static_cast(source.data)[0] = 0x5au; + if (mode == 2) static_cast(metadata.data)[0] = 0x00u; + if (mode == 3) { + const std::array same{std::byte{0xcc}}; + AgcDriver::GuestMemory::Write(cmask.Address(), same); + } + if (mode == 9) static_cast(fmask->data)[0] = 0x5au; + if (mode == 10) { + const std::array same{std::byte{0x11}}; + AgcDriver::GuestMemory::Write(fmask->Address(), same); + } + const std::string_view storeRefusal = mode >= 9 ? "writes to a resident native FMASK" : "writes to a resident multisampled DCC"; + try { + Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find(storeRefusal) != std::string_view::npos; + if (refused) expectRange(error.what(), plane, block->Address(), bytes, samples); + } + Require(refused, mode == 3 ? "a same-value driver store over a compared CMASK range did not invalidate multisampled DCC residency" : mode == 10 ? "a same-value driver store over a compared FMASK range did not invalidate its residency" : mode == 9 ? "a CPU store of a new byte over a compared FMASK range did not invalidate its residency" : "a CPU store of new bytes over a compared multisampled DCC plane did not invalidate its residency"); + if (mode >= 9) std::printf("8 samples, %s: refused at reuse\n", fmaskCase); + programCount = 0; + } + } + return nullptr; +} +#endif + int main(int argc, char** argv) { try { const auto device = OpenVulkanTestDevice(); @@ -933,6 +1098,29 @@ int main(int argc, char** argv) { std::cout << "Multisampled DCC fast clear tests passed\n"; return 0; } + if ((argc == 2 || argc == 3) && std::string_view(argv[1]) == "--dcc-compared-import") { + const auto samples = argc == 2 ? 0u : std::string_view(argv[2]) == "2" ? 2u : std::string_view(argv[2]) == "4" ? 4u : std::string_view(argv[2]) == "8" ? 8u : 0u; + Require(argc == 2 || samples != 0, "unknown DCC test sample count"); +#ifdef _WIN32 + std::puts("skipped, Linux comparison mode only: Windows host imports settle on a second queue or keep their write watch"); + return VulkanTestSkipped; +#else + const char* skipped = nullptr; + try { + skipped = DccComparedImportTests(*device, samples); + } 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 the requested multisample color attachments: %s\n", error.what()); + return VulkanTestSkipped; + } + if (skipped != nullptr) { + std::puts(skipped); + return VulkanTestSkipped; + } + std::cout << "Multisampled DCC compared host import tests passed\n"; + return 0; +#endif + } if (argc == 2 && std::string_view(argv[1]) == "--dithered8") { try { DitheredCoverage(*device); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp index 6faa64b11e..5dc96d71c0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" @@ -7,7 +8,9 @@ #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" @@ -826,6 +829,45 @@ void RunSharedImport(AgcDriver::VulkanDevice& device, std::uint8_t* block) { } +void RunMipmappedTarget(AgcDriver::VulkanDevice& device, std::uint8_t* block) { +#ifndef _WIN32 + constexpr std::uint32_t ChainSide = 64; + constexpr std::uint32_t ChainLevels = 7; + auto* texels = block; + auto* keys = block + KeysOffset; + const auto surface = AddressOf(texels); + const auto meta = AddressOf(keys); + std::memset(texels, 0x55, SurfaceBytes); + std::memset(keys, 0x00, KeyBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + AgcDriver::Registers cx; + cx[0x318] = static_cast(surface >> 8u); + cx[0x390] = static_cast(surface >> 40u); + cx[0x31b] = 0; + cx[0x31c] = 0x10028028u; + cx[0x31d] = 0; + cx[0x3b0] = ((ChainLevels - 1u) << 28u) | ((ChainSide - 1u) << 14u) | (ChainSide - 1u); + cx[0x3b8] = 0x9000000u | (TileRenderTarget64KB << 14u) | (1u << 30u); + cx[0x325] = static_cast(meta >> 8u); + cx[0x3a8] = static_cast(meta >> 40u); + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + const auto resource = AgcDriver::Graphics::SurfaceForTarget(color); + Require(color.mipCount == ChainLevels && color.dccAddress == 0 && resource.dccAddress == meta, "the resident surface of a mipmapped DCC color target has no keys"); + const auto bytes = AgcDriver::Graphics::DescribeSurface(resource).guestBytes; + Require(bytes <= SurfaceBytes, "the mip chain does not fit the test surface"); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + const auto image = AgcDriver::Graphics::CachedStorageSurface(context, resource); + Require(image->UploadedKeys() == DccKeys::Clear0000, "the resident image of a 0000-cleared mipmapped target was not cleared from its keys"); + image->MarkDirty(); + Require(AgcDriver::Graphics::CurrentDccKeys(meta, bytes) == DccKeys::Uncompressed, "the first write into a mipmapped DCC target left its keys cleared"); + Require(AgcDriver::Graphics::TextureClearKeys(resource, bytes) == DccKeys::Uncompressed, "a sampled view of a rendered mipmapped DCC target still reads its clear"); +#else + static_cast(device); + static_cast(block); +#endif +} + int main() { try { #ifdef _WIN32 @@ -866,6 +908,9 @@ int main() { std::optional uniform; uniform.emplace(false); RunUniformKeysBehindKernel(*device, uniform->Data()); + std::optional chain; + chain.emplace(false); + RunMipmappedTarget(*device, chain->Data()); #ifdef _WIN32 shared.emplace(true, true); RunSharedImport(*device, shared->Data()); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthBoundsEmulationDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthBoundsEmulationDraw.cpp new file mode 100644 index 0000000000..eaddbfcf25 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthBoundsEmulationDraw.cpp @@ -0,0 +1,173 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "Recompiler.hpp" +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +namespace Harness = DepthFastClearHarness; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +struct Bounds { + float min; + float max; +}; + +struct Case { + float stored; + Bounds bounds; + bool covered; + const char* what; +}; + +alignas(4096) std::array, 16> Depths{}; +alignas(256) std::array, 3>, 64> VertexSets{}; +std::size_t vertexSets = 0; + +std::uint32_t Draw(AgcDriver::VulkanDevice& device, std::uint64_t depth, float z, std::optional bounds, bool emulate) { + Require(vertexSets < VertexSets.size(), "the test ran out of vertex sets"); + auto& vertices = VertexSets[vertexSets++]; + vertices = {{{-1.0f, -1.0f, z, 1.0f}, {3.0f, -1.0f, z, 1.0f}, {-1.0f, 3.0f, z, 1.0f}}}; + Harness::Pixels.fill(std::byte{0}); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Harness::Pixels.data()), {Harness::Width, Harness::Height}, VK_FORMAT_R8G8B8A8_UNORM, Harness::Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.depth = AgcDriver::Graphics::DepthTarget{depth, 0, {Harness::Width, Harness::Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + state.depthTest = !bounds.has_value(); + state.depthWrite = !bounds.has_value(); + state.depthCompare = VK_COMPARE_OP_ALWAYS; + if (bounds) { + state.depthBoundsTest = true; + state.minDepthBounds = bounds->min; + state.maxDepthBounds = bounds->max; + } + state.renderExtent = {Harness::Width, Harness::Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Harness::Height), static_cast(Harness::Width), -static_cast(Harness::Height), 0.0f, 1.0f}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Harness::Width, Harness::Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = true; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + if (bounds && emulate) { + auto context = device.graphicsContext(); + context.depthBounds = false; + context.depthRangeUnrestricted = false; + AgcDriver::Graphics::ApplyDepthBoundsEmulation(context, state, pixel); + Require(!state.depthBoundsTest && state.depthTest && !state.depthWrite, "the depth bounds test was not emulated"); + } + + auto target = device.Target(); + target.depthRangeRemap = false; + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = Harness::BufferDescriptor(vertices.data(), 16u, static_cast(vertices.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(Harness::VertexCode.data()), std::as_bytes(std::span(Harness::VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(Harness::VertexCode.data()), Harness::VertexCode, 0, {}}, + {32, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + std::vector pixelUserData(4, 0u); + const std::array pixelMemory{{{reinterpret_cast(Harness::PixelCode.data()), std::as_bytes(std::span(Harness::PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(Harness::PixelCode.data()), Harness::PixelCode, 0, {}}, + {32, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(vertices.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); + std::uint32_t covered = 0; + for (std::size_t index = 0; index < Harness::Pixels.size() / 4u; ++index) { + if (std::to_integer(Harness::Pixels[index * 4u]) == 255u) ++covered; + } + return covered; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto context = device->graphicsContext(); + const float below = std::nextafter(0.25f, 0.0f); + const float above = std::nextafter(0.25f, 1.0f); + const std::array cases{{ + {0.25f, {0.0f, 0.25f}, true, "stored 0.25, upper bound equal to it"}, + {0.25f, {0.0f, below}, false, "stored 0.25, upper bound one step below it"}, + {0.25f, {0.25f, 1.0f}, true, "stored 0.25, lower bound equal to it"}, + {0.25f, {above, 1.0f}, false, "stored 0.25, lower bound one step above it"}, + {0.000125f, {0.0f, 0.000125f}, true, "stored 0.000125 inside [0, 0.000125]"}, + {0.5f, {0.0f, 0.000125f}, false, "stored 0.5 outside [0, 0.000125]"}, + {0.5f, {0.000125f, 1.0f}, true, "stored 0.5 inside [0.000125, 1]"}, + {0.0f, {0.000125f, 1.0f}, false, "stored 0 outside [0.000125, 1]"}, + {0.0f, {0.0f, 1.0f}, true, "stored 0 inside [0, 1]"}, + {1.0f, {0.0f, 1.0f}, true, "stored 1 inside [0, 1]"}, + {1.0f, {0.0f, 0.5f}, false, "stored 1 outside [0, 0.5]"}, + {0.75f, {0.5f, 1.0f}, true, "stored 0.75 inside [0.5, 1]"}, + }}; + std::size_t compared = 0; + for (std::size_t index = 0; index < cases.size(); ++index) { + const auto& test = cases[index]; + const auto depth = static_cast(reinterpret_cast(Depths[index].data())); + Require(Draw(*device, depth, test.stored, std::nullopt, false) == Harness::Covered, "the draw that stores the depth did not cover the target"); + const auto expected = test.covered ? Harness::Covered : 0u; + for (const bool emulate : {true, false}) { + if (!emulate && !context.depthBounds) continue; + const auto passed = Draw(*device, depth, 0.5f, test.bounds, emulate); + if (passed != expected) { + char detail[256]; + std::snprintf(detail, sizeof(detail), "%s (%s): %u of %u pixels passed, expected %s", test.what, emulate ? "emulated" : "native", passed, Harness::Covered, test.covered ? "all" : "none"); + throw std::runtime_error(detail); + } + if (!emulate) ++compared; + } + } + std::printf("depth bounds emulation draw tests passed (%zu cases, %zu compared with the native test)\n", cases.size(), compared); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp index 4145b544b5..4d791ac7f7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp @@ -223,6 +223,65 @@ private: std::uint8_t* block = nullptr; }; +class SparseLayers { +public: + SparseLayers() { +#ifdef _WIN32 + memory = static_cast(GuestArena::GuestArenaAllocateAtOrAbove_nid_postfix(GuestArena::GuestArenaHighestFree_nid_no_patch(BlockBytes, 65536), BlockBytes, 65536)); +#else + constexpr std::uintptr_t alignment = 65536; + void* raw = mmap(nullptr, BlockBytes + alignment, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot reserve sparse depth layers"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast(aligned + BlockBytes), begin + alignment - aligned); + memory = reinterpret_cast(aligned); +#endif + Require(memory != nullptr, "cannot reserve sparse depth layers"); + } + + ~SparseLayers() { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(memory, BlockBytes); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(), BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, BlockBytes); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, BlockBytes); + munmap(memory, BlockBytes); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(), BlockBytes); +#endif + } + + void Commit(std::size_t offset, std::size_t bytes) { +#ifdef _WIN32 + GuestArena::GuestArenaCommit_nid_postfix(memory + offset, bytes, PAGE_READWRITE, 65536); +#else + Require(mmap(memory + offset, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) != MAP_FAILED, "cannot commit sparse depth layer pages"); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory + offset, bytes); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address() + offset, bytes); + } + + void DecommitGuestLayer() { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(memory + LayerBytes, LayerBytes); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory + LayerBytes, LayerBytes); + Require(mmap(memory + LayerBytes, LayerBytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) != MAP_FAILED, "cannot decommit sparse depth layer pages"); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address() + LayerBytes, LayerBytes); + } + + std::uint8_t* Data() const { return memory; } + std::uint64_t Address() const { return reinterpret_cast(memory); } + SparseLayers(const SparseLayers&) = delete; + SparseLayers& operator=(const SparseLayers&) = delete; + +private: + std::uint8_t* memory = nullptr; +}; + std::atomic watchedBegin{0}; std::atomic watchedEnd{0}; std::atomic watchedReads{0}; @@ -308,6 +367,59 @@ void Run(const Context& context, const WatchedBlock& block) { AgcDriver::GuestMemory::SetFlushHook(nullptr); } +void SparseGuestLayers(const Context& context) { + ClearDepthSurfaces(context.device); + SparseLayers block; + block.Commit(0, LayerBytes); + FillLayer(block.Data(), 1.0f); + const auto address = block.Address(); + DepthSurfaceView(context, DepthTarget{address, 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); + const auto resource = ArrayOver(address); + watchedBegin = address + LayerBytes; + watchedEnd = address + BlockBytes; + AgcDriver::GuestMemory::SetFlushHook(&CountReads); + const auto sample = [&](const char* when, bool changed) { + AgcDriver::GuestMemory::BumpCollectEpoch(); + watchedReads = 0; + Require(DepthSurfaceTexture(context, words, resource, components) != nullptr, "sparse depth layers were not sampled"); + Require((watchedReads.load() != 0) == changed, std::string("sparse depth layer read mismatch ") + when); + }; + try { + sample("at first use", true); + sample("with the guest layer still uncommitted", false); + block.Commit(LayerBytes, 65536); + const float value = 0.25f; + std::memcpy(block.Data() + LayerBytes, &value, sizeof(value)); + sample("after committing part of the guest layer", true); + block.Data()[LayerBytes + 64] = 1; + sample("after a CPU write to the committed part", true); + AgcDriver::GuestMemory::MarkWritten(address + LayerBytes + 128, 4); + sample("after a driver store to the committed part", true); + block.DecommitGuestLayer(); + sample("after decommitting the guest layer", true); + sample("with the guest layer still decommitted", false); + block.Commit(LayerBytes, 65536); + sample("after committing the same pages again", true); + block.DecommitGuestLayer(); + block.Commit(LayerBytes, 65536); + sample("after replacing a mapping with the same committed range", true); + block.DecommitGuestLayer(); + block.Commit(LayerBytes, LayerBytes); + FillLayer(block.Data() + LayerBytes, 0.5f); + sample("after fully committing the guest layer", true); + sample("with the fully committed layer unchanged", false); + block.DecommitGuestLayer(); + sample("after decommitting a fully committed guest layer", true); + sample("with the fully committed layer now empty", false); + } catch (...) { + AgcDriver::GuestMemory::SetFlushHook(nullptr); + ClearDepthSurfaces(context.device); + throw; + } + AgcDriver::GuestMemory::SetFlushHook(nullptr); + ClearDepthSurfaces(context.device); +} + void RecordedUploads(const Context& context, const WatchedBlock& block) { const auto resource = ArrayOver(block.Address()); Recorder recorder(context); @@ -418,6 +530,7 @@ int main() { RecordedUploads(context, block); RunCubeGather(context, block); RunCubeTiledSlice(context, block); + SparseGuestLayers(context); } device.reset(); std::puts("depth plane guest layer tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DeviceQueuePlan.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DeviceQueuePlan.cpp index ca3c05ab27..032a78ccc8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DeviceQueuePlan.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DeviceQueuePlan.cpp @@ -34,16 +34,40 @@ VkQueueFamilyProperties family(VkQueueFlags flags, std::uint32_t queues) { } std::string describe(const AgcDriver::DeviceQueuePlan& plan) { - return "main " + std::to_string(plan.mainCount) + " queues, settle " + (plan.settle ? std::to_string(plan.settleFamily) + ":" + std::to_string(plan.settleIndex) : std::string("none")) + ", sparse " + (!plan.sparseBinding ? std::string("none") : plan.sparseFamily == Own ? std::string("on the main queue") : std::to_string(plan.sparseFamily) + ":" + std::to_string(plan.sparseIndex)); + return "main " + std::to_string(plan.mainCount) + " queues, present " + (plan.present ? std::to_string(plan.presentIndex) : std::string("none")) + ", settle " + (plan.settle ? std::to_string(plan.settleFamily) + ":" + std::to_string(plan.settleIndex) : std::string("none")) + ", sparse " + (!plan.sparseBinding ? std::string("none") : plan.sparseFamily == Own ? std::string("on the main queue") : std::to_string(plan.sparseFamily) + ":" + std::to_string(plan.sparseIndex)); } void check(const Case& test) { - const auto plan = AgcDriver::PlanDeviceQueues(test.families, 0, test.settles, true); - const auto dense = AgcDriver::PlanDeviceQueues(test.families, 0, test.settles, false); + const auto plan = AgcDriver::PlanDeviceQueues(test.families, 0, test.settles, true, false); + const auto dense = AgcDriver::PlanDeviceQueues(test.families, 0, test.settles, false, false); Require(plan.mainCount == dense.mainCount && plan.settle == dense.settle && plan.settleFamily == dense.settleFamily && plan.settleIndex == dense.settleIndex, std::string("device queue plan, ") + test.name + ": sparse binding changed the queues: " + describe(dense) + " without it, " + describe(plan) + " with it"); Require(!dense.sparseBinding && dense.sparseFamily == Own, std::string("device queue plan, ") + test.name + ": a device without sparse binding got a sparse queue: " + describe(dense)); const bool expected = plan.mainCount == test.mainCount && plan.settle == test.settle && (!test.settle || (plan.settleFamily == test.settleFamily && plan.settleIndex == test.settleIndex)) && plan.sparseBinding == test.sparseBinding && plan.sparseFamily == test.sparseFamily && (test.sparseFamily == Own || plan.sparseIndex == test.sparseIndex); - Require(expected, std::string("device queue plan, ") + test.name + ": got " + describe(plan)); + Require(expected && !plan.present, std::string("device queue plan, ") + test.name + ": got " + describe(plan)); + const auto windowed = AgcDriver::PlanDeviceQueues(test.families, 0, test.settles, true, true); + const bool kept = windowed.settle == plan.settle && windowed.settleFamily == plan.settleFamily && windowed.settleIndex == plan.settleIndex && windowed.sparseBinding == plan.sparseBinding && windowed.sparseFamily == plan.sparseFamily && windowed.sparseIndex == plan.sparseIndex; + const bool counted = windowed.present ? windowed.presentIndex == plan.mainCount && windowed.mainCount == plan.mainCount + 1 : windowed.mainCount == plan.mainCount && plan.mainCount >= test.families[0].queueCount; + Require(kept && counted, std::string("device queue plan, ") + test.name + ": a window changed more than the presentation queue: " + describe(plan) + " without it, " + describe(windowed) + " with it"); +} + +struct WindowCase { + const char* name; + std::vector families; + bool settles; + std::uint32_t mainCount; + bool present; + std::uint32_t presentIndex; + bool settle; + std::uint32_t settleFamily; + std::uint32_t settleIndex; + std::uint32_t sparseFamily; + std::uint32_t sparseIndex; +}; + +void check(const WindowCase& test) { + const auto plan = AgcDriver::PlanDeviceQueues(test.families, 0, test.settles, true, true); + const bool expected = plan.mainCount == test.mainCount && plan.present == test.present && (!test.present || plan.presentIndex == test.presentIndex) && plan.settle == test.settle && (!test.settle || (plan.settleFamily == test.settleFamily && plan.settleIndex == test.settleIndex)) && plan.sparseFamily == test.sparseFamily && (test.sparseFamily == Own || plan.sparseIndex == test.sparseIndex); + Require(expected, std::string("device queue plan with a window, ") + test.name + ": got " + describe(plan)); } } @@ -62,6 +86,15 @@ int main() { {"only the settle queue binds", {family(Graphics, 1), family(VK_QUEUE_TRANSFER_BIT | Sparse, 1)}, true, 1, true, 1, 0, false, Own, 0}, }; for (const auto& test : cases) check(test); + const std::vector windowCases{ + {"one queue with a window", {family(Graphics | Sparse, 1)}, true, 1, false, 0, false, 0, 0, Own, 0}, + {"two queues, no settles, with a window", {family(Graphics, 2)}, false, 2, true, 1, false, 0, 0, Own, 0}, + {"two queues taken by the main and settle queues, with a window", {family(Graphics, 2)}, true, 2, false, 0, true, 0, 1, Own, 0}, + {"settle on the main family, three queues, with a window", {family(Graphics | Sparse, 3), family(VK_QUEUE_TRANSFER_BIT | Sparse, 1)}, true, 3, true, 2, true, 0, 1, 1, 0}, + {"one main queue and settle on another family, with a window", {family(Graphics, 1), family(VK_QUEUE_TRANSFER_BIT, 1)}, true, 1, false, 0, true, 1, 0, Own, 0}, + {"sparse-only family preferred, with a window", {family(Graphics | Sparse, 16), family(VK_QUEUE_TRANSFER_BIT | Sparse, 2), family(Sparse, 1)}, true, 3, true, 2, true, 0, 1, 2, 0}, + }; + for (const auto& test : windowCases) check(test); std::puts("device queue plan tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DispatchAheadAdoptCheck.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchAheadAdoptCheck.cpp new file mode 100644 index 0000000000..e2453a66f0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchAheadAdoptCheck.cpp @@ -0,0 +1,208 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array CounterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000281u, 0xe0c80000u, 0x80020000u, 0xbf810000u}; +alignas(256) std::array StoreCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000202u, 0xe0700000u, 0x80020000u, 0xbf810000u}; +alignas(256) std::array SpinCode{0xf4080200u, 0xfa000000u, 0xbe840381u, 0x9304ff04u, 0x0019660du, 0x8004ff04u, 0x3c6ef35fu, 0x80828102u, 0xbf078002u, 0xbf85fff9u, 0xbf8cc07fu, 0x7e000204u, 0xe0700000u, 0x80020000u, 0xbf810000u}; + +constexpr std::uint32_t Compute = 0x20u; +constexpr std::uint32_t OtherCompute = 0x28u; +constexpr std::uint32_t SpinIterations = 1u << 20u; + +alignas(256) std::array Stale{}; +alignas(256) std::array Fresh{}; +alignas(256) std::array Target{}; +alignas(256) std::array TargetDescriptor{}; +alignas(256) std::array Spun{}; +alignas(256) std::array SpunDescriptor{}; +alignas(256) volatile std::uint32_t Gate = 1; +alignas(256) volatile std::uint32_t Done = 0; +alignas(256) volatile std::uint32_t OtherDone = 0; + +enum class Window { None, CpuStore, QueuedGpuStore }; + +std::atomic Armed{Window::None}; +std::uint32_t SpinDispatches = 16; +std::vector OtherWords; +bool SawPending = false; + +std::uint32_t Low(const volatile void* pointer) { + return static_cast(reinterpret_cast(pointer)); +} + +std::uint32_t High(const volatile void* pointer) { + return static_cast(static_cast(reinterpret_cast(pointer)) >> 32u); +} + +std::array Descriptor(const void* pointer, std::uint32_t bytes) { + return {Low(pointer), High(pointer) & 0xffffu, bytes, 0x31016facu}; +} + +void SetShaderRegister(std::vector& words, std::uint32_t offset, std::uint32_t value) { + words.insert(words.end(), {0xc0017600u, offset, value}); +} + +void Program(std::vector& words, const void* code, std::initializer_list userData) { + const auto address = static_cast(reinterpret_cast(code)); + SetShaderRegister(words, 0x20c, static_cast(address >> 8u)); + SetShaderRegister(words, 0x20d, static_cast(address >> 40u) & 0xffu); + for (const std::uint32_t offset : {0x207u, 0x208u, 0x209u}) SetShaderRegister(words, offset, 1); + SetShaderRegister(words, 0x212, 0); + SetShaderRegister(words, 0x213, static_cast(userData.size()) << 1u); + std::uint32_t offset = 0x240; + for (const auto value : userData) SetShaderRegister(words, offset++, value); +} + +void Dispatch(std::vector& words, const void* code, std::initializer_list userData) { + Program(words, code, userData); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); +} + +void WriteData(std::vector& words, const volatile std::uint32_t* target, std::uint32_t value) { + words.insert(words.end(), {0xc0033700u, 0x00100200u, Low(target), High(target), value}); +} + +void Submit(std::uint32_t queue, std::vector& words) { + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(queue, &packet) == 0, "compute submit failed"); +} + +void QueueStoreAhead() { + OtherWords.clear(); + for (std::uint32_t index = 0; index < SpinDispatches; ++index) Dispatch(OtherWords, SpinCode.data(), {Low(SpunDescriptor.data()), High(SpunDescriptor.data()), SpinIterations}); + Dispatch(OtherWords, StoreCode.data(), {Low(TargetDescriptor.data()), High(TargetDescriptor.data()), Low(Fresh.data())}); + WriteData(OtherWords, &OtherDone, 1); + Submit(OtherCompute, OtherWords); + const auto start = std::chrono::steady_clock::now(); + while (std::chrono::steady_clock::now() - start < std::chrono::seconds(5) && OtherDone == 0) { + if (AgcDriver::DriverDetail::Driver::QueryPendingSpan(reinterpret_cast(Target.data()), sizeof(Target), {}) != AgcDriver::ShaderMemory::PendingWrite::None) { + SawPending = true; + return; + } + std::this_thread::yield(); + } +} + +void BeforeAdoption() { + const auto window = Armed.exchange(Window::None); + if (window == Window::CpuStore) reinterpret_cast(Target[0]) = Low(Fresh.data()); + else if (window == Window::QueuedGpuStore) QueueStoreAhead(); +} + +void WaitIdle() { + AgcDriverWaitIdle_nid_postfix(); + for (const auto* buffer : {&Stale, &Fresh, &Target}) AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(buffer->data()), sizeof(*buffer)); +} + +void Reset() { + WaitIdle(); + Stale = {}; + Fresh = {}; + Target = Descriptor(Stale.data(), sizeof(Stale)); + Done = 0; + OtherDone = 0; + SawPending = false; +} + +void Gated(Window window) { + Reset(); + std::vector words; + words.insert(words.end(), {0xc0053c00u, 0x13u, Low(&Gate), High(&Gate), 1u, 0xffffffffu, 0x19u}); + Dispatch(words, CounterCode.data(), {Low(Target.data()), High(Target.data())}); + WriteData(words, &Done, 1); + Armed = window; + Submit(Compute, words); + WaitIdle(); + Armed = Window::None; + Require(Done == 1, "the gated submission did not finish"); +} + +std::string Counts() { + return "(stale " + std::to_string(Stale[0]) + ", fresh " + std::to_string(Fresh[0]) + ")"; +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); + Require(High(Stale.data()) == High(Fresh.data()), "the stale and fresh buffers differ in their high address word"); + TargetDescriptor = Descriptor(Target.data(), sizeof(Target)); + SpunDescriptor = Descriptor(Spun.data(), sizeof(Spun)); + AgcDriverSetAheadAdoptHook_nid_postfix(&BeforeAdoption); + Reset(); + std::vector warmUp; + Dispatch(warmUp, CounterCode.data(), {Low(Target.data()), High(Target.data())}); + WriteData(warmUp, &Done, 1); + Submit(Compute, warmUp); + std::vector otherWarmUp; + Dispatch(otherWarmUp, SpinCode.data(), {Low(SpunDescriptor.data()), High(SpunDescriptor.data()), SpinIterations}); + Dispatch(otherWarmUp, StoreCode.data(), {Low(TargetDescriptor.data()), High(TargetDescriptor.data()), Low(Fresh.data())}); + Submit(OtherCompute, otherWarmUp); + WaitIdle(); + Require(Done == 1 && Target[0] == Low(Fresh.data()), "the warm-up dispatches did not run"); + + Gated(Window::None); + Require(Stale[0] == 1 && Fresh[0] == 0, "the gated dispatch without a store in the window did not count into the stale buffer " + Counts()); + const auto adopted = AgcDriverDispatchesAdoptedAhead_nid_postfix(); + Require(adopted != 0, "the gated dispatch was not resolved ahead and adopted"); + + auto rejected = AgcDriverDispatchesRejectedAhead_nid_postfix(); + Gated(Window::CpuStore); + Require(Fresh[0] == 1 && Stale[0] == 0, "a dispatch resolved ahead whose descriptor the CPU replaced before its adoption used the old descriptor " + Counts()); + Require(AgcDriverDispatchesRejectedAhead_nid_postfix() == rejected + 1, "the CPU store in the adoption window was not caught by the adoption check"); + std::cout << "cpu store before adoption: passed\n"; + + bool conclusive = false; + for (int attempt = 0; attempt < 4 && !conclusive; ++attempt, SpinDispatches *= 4) { + rejected = AgcDriverDispatchesRejectedAhead_nid_postfix(); + Gated(Window::QueuedGpuStore); + conclusive = SawPending; + if (!conclusive) { + std::cout << "queued gpu store before adoption: the store finished before the adoption with " << SpinDispatches << " spin dispatches, retrying\n"; + continue; + } + Require(Fresh[0] == 1 && Stale[0] == 0, "a dispatch resolved ahead whose descriptor a GPU store queued on another queue replaces before its adoption used the old descriptor " + Counts()); + Require(AgcDriverDispatchesRejectedAhead_nid_postfix() == rejected + 1, "the queued GPU store in the adoption window was not caught by the adoption check"); + } + AgcDriverSetAheadAdoptHook_nid_postfix(nullptr); + if (!conclusive) { + std::cout << "queued gpu store before adoption: never observed the store pending at the adoption\n"; + AgcDriverShutdown_nid_postfix(); + return VulkanTestSkipped; + } + std::cout << "queued gpu store before adoption: passed\n"; + std::cout << "dispatch ahead adopt check test passed\n"; + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32ConvertDenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32ConvertDenormalModes.cpp new file mode 100644 index 0000000000..9dcc8b2bf9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32ConvertDenormalModes.cpp @@ -0,0 +1,212 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 16; +constexpr std::uint32_t Columns = 12; +constexpr std::uint32_t Float64Low = 5; +constexpr std::uint32_t Float64High = 6; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e141104u, 0x7e160f04u, 0x7e181904u, + 0x7e1a1b04u, 0x7e1c7f04u, 0xd7680011u, 0x00020b04u, 0xd7690012u, 0x00020b04u, 0xd55e0013u, 0x02010104u, + 0x5e280b04u, 0x7e2a1504u, 0xe0781000u, 0x80010a02u, 0xe0781010u, 0x80010e02u, 0xe0781020u, 0x80011202u, + 0xbf810000u, +}; + +alignas(256) constexpr std::array Float64Code{ + 0x34020084u, 0x34040086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e1e2104u, 0xe0781000u, 0x80010a02u, + 0xe0781010u, 0x80010e02u, 0xe0781020u, 0x80011202u, 0xbf810000u, +}; + +constexpr const char* Names[Columns] = { + "v_cvt_i32_f32 v10, v4", + "v_cvt_u32_f32 v11, v4", + "v_cvt_rpi_i32_f32 v12, v4", + "v_cvt_flr_i32_f32 v13, v4", + "v_frexp_exp_i32_f32 v14, v4", + "v_cvt_f64_f32 v[15:16], v4 (low)", + "v_cvt_f64_f32 v[15:16], v4 (high)", + "v_cvt_pknorm_i16_f32 v17, v4, v5", + "v_cvt_pknorm_u16_f32 v18, v4, v5", + "v_cvt_pk_u8_f32 v19, v4, 0, 0", + "v_cvt_pkrtz_f16_f32 v20, v4, v5", + "v_cvt_f16_f32 v21, v4", +}; + +struct ConvertVector { + std::uint32_t operands[2]; + std::uint32_t denormalsFlushed[Columns]; + std::uint32_t denormalsKept[Columns]; +}; + +constexpr std::array Vectors{{ + {{0x00000001u, 0x00400000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffff6cu, 0x00000000u, 0x36a00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {{0x007fffffu, 0x80000001u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffff82u, 0xc0000000u, 0x380fffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u}}, + {{0x00400000u, 0x3f800000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7fff0000u, 0xffff0000u, 0x00000000u, 0x3c000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffff82u, 0x00000000u, 0x38000000u, 0x7fff0000u, 0xffff0000u, 0x00000000u, 0x3c000000u, 0x00000000u}}, + {{0x80000001u, 0x007fffffu}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0xffffff6cu, 0x00000000u, 0xb6a00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u}}, + {{0x807fffffu, 0x00000001u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0xffffff82u, 0xc0000000u, 0xb80fffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u}}, + {{0x80400000u, 0xbf000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0xc0000000u, 0x00000000u, 0x00000000u, 0xb8008000u, 0x00008000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0xffffff82u, 0x00000000u, 0xb8000000u, 0xc0000000u, 0x00000000u, 0x00000000u, 0xb8008000u, 0x00008000u}}, + {{0x00800000u, 0x80400000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffff83u, 0x00000000u, 0x38100000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffff83u, 0x00000000u, 0x38100000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u}}, + {{0x80800000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0xffffff83u, 0x00000000u, 0xb8100000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0xffffff83u, 0x00000000u, 0xb8100000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u}}, + {{0x3f000000u, 0x00000001u}, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3fe00000u, 0x00004000u, 0x00008000u, 0x00000000u, 0x00003800u, 0x00003800u}, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3fe00000u, 0x00004000u, 0x00008000u, 0x00000000u, 0x00003800u, 0x00003800u}}, + {{0xbfc00000u, 0x807fffffu}, {0xffffffffu, 0x00000000u, 0xffffffffu, 0xfffffffeu, 0x00000001u, 0x00000000u, 0xbff80000u, 0x00008001u, 0x00000000u, 0x00000000u, 0x8000be00u, 0x0000be00u}, {0xffffffffu, 0x00000000u, 0xffffffffu, 0xfffffffeu, 0x00000001u, 0x00000000u, 0xbff80000u, 0x00008001u, 0x00000000u, 0x00000000u, 0x8000be00u, 0x0000be00u}}, + {{0x3f800000u, 0x80000002u}, {0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000000u, 0x3ff00000u, 0x00007fffu, 0x0000ffffu, 0x00000001u, 0x80003c00u, 0x00003c00u}, {0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000000u, 0x3ff00000u, 0x00007fffu, 0x0000ffffu, 0x00000001u, 0x80003c00u, 0x00003c00u}}, + {{0x00000000u, 0x00000002u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {{0x80000000u, 0x3f800000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x7fff0000u, 0xffff0000u, 0x00000000u, 0x3c008000u, 0x00008000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x7fff0000u, 0xffff0000u, 0x00000000u, 0x3c008000u, 0x00008000u}}, + {{0x33800000u, 0x00000001u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffffe9u, 0x00000000u, 0x3e700000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffffe9u, 0x00000000u, 0x3e700000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u}}, + {{0x477fe000u, 0x80000001u}, {0x0000ffe0u, 0x0000ffe0u, 0x0000ffe0u, 0x0000ffe0u, 0x00000010u, 0x00000000u, 0x40effc00u, 0x00007fffu, 0x0000ffffu, 0x000000ffu, 0x80007bffu, 0x00007bffu}, {0x0000ffe0u, 0x0000ffe0u, 0x0000ffe0u, 0x0000ffe0u, 0x00000010u, 0x00000000u, 0x40effc00u, 0x00007fffu, 0x0000ffffu, 0x000000ffu, 0x80007bffu, 0x00007bffu}}, + {{0x501502f9u, 0x00800000u}, {0x7fffffffu, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0x00000022u, 0x20000000u, 0x4202a05fu, 0x00007fffu, 0x0000ffffu, 0x000000ffu, 0x00007bffu, 0x00007c00u}, {0x7fffffffu, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0x00000022u, 0x20000000u, 0x4202a05fu, 0x00007fffu, 0x0000ffffu, 0x000000ffu, 0x00007bffu, 0x00007c00u}}, + {{0xd01502f9u, 0x007fffffu}, {0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000022u, 0x20000000u, 0xc202a05fu, 0x00008001u, 0x00000000u, 0x00000000u, 0x0000fbffu, 0x0000fc00u}, {0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000022u, 0x20000000u, 0xc202a05fu, 0x00008001u, 0x00000000u, 0x00000000u, 0x0000fbffu, 0x0000fc00u}}, + {{0x7f800000u, 0xbfc00000u}, {0x7fffffffu, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0x00000000u, 0x00000000u, 0x7ff00000u, 0x80017fffu, 0x0000ffffu, 0x000000ffu, 0xbe007c00u, 0x00007c00u}, {0x7fffffffu, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0x00000000u, 0x00000000u, 0x7ff00000u, 0x80017fffu, 0x0000ffffu, 0x000000ffu, 0xbe007c00u, 0x00007c00u}}, + {{0xff800000u, 0x00000001u}, {0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0xfff00000u, 0x00008001u, 0x00000000u, 0x00000000u, 0x0000fc00u, 0x0000fc00u}, {0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0xfff00000u, 0x00008001u, 0x00000000u, 0x00000000u, 0x0000fc00u, 0x0000fc00u}}, + {{0x7fc00000u, 0x80400000u}, {0x00000000u, 0x00000000u, 0x7fffffffu, 0x7fffffffu, 0x00000000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80007e00u, 0x00007e00u}, {0x00000000u, 0x00000000u, 0x7fffffffu, 0x7fffffffu, 0x00000000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80007e00u, 0x00007e00u}}, + {{0xffc00000u, 0x00400000u}, {0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0xfff80000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000fe00u, 0x0000fe00u}, {0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0xfff80000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000fe00u, 0x0000fe00u}}, + {{0x4f800000u, 0x80000000u}, {0x7fffffffu, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0x00000021u, 0x00000000u, 0x41f00000u, 0x00007fffu, 0x0000ffffu, 0x000000ffu, 0x80007bffu, 0x00007c00u}, {0x7fffffffu, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0x00000021u, 0x00000000u, 0x41f00000u, 0x00007fffu, 0x0000ffffu, 0x000000ffu, 0x80007bffu, 0x00007c00u}}, + {{0xcf000000u, 0x007fffffu}, {0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000020u, 0x00000000u, 0xc1e00000u, 0x00008001u, 0x00000000u, 0x00000000u, 0x0000fbffu, 0x0000fc00u}, {0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000020u, 0x00000000u, 0xc1e00000u, 0x00008001u, 0x00000000u, 0x00000000u, 0x0000fbffu, 0x0000fc00u}}, + {{0x3f7fffffu, 0x80000001u}, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0xe0000000u, 0x3fefffffu, 0x00007fffu, 0x0000ffffu, 0x00000001u, 0x80003bffu, 0x00003c00u}, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0xe0000000u, 0x3fefffffu, 0x00007fffu, 0x0000ffffu, 0x00000001u, 0x80003bffu, 0x00003c00u}}, + {{0x00000002u, 0x3f800000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7fff0000u, 0xffff0000u, 0x00000000u, 0x3c000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffff6du, 0x00000000u, 0x36b00000u, 0x7fff0000u, 0xffff0000u, 0x00000000u, 0x3c000000u, 0x00000000u}}, + {{0x80000002u, 0x00000003u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0xffffff6du, 0x00000000u, 0xb6b00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u}}, + {{0x00000001u, 0x00000001u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffff6cu, 0x00000000u, 0x36a00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {{0x007fffffu, 0x807fffffu}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffff82u, 0xc0000000u, 0x380fffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u}}, + {{0x3f000000u, 0x80000001u}, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3fe00000u, 0x00004000u, 0x00008000u, 0x00000000u, 0x80003800u, 0x00003800u}, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3fe00000u, 0x00004000u, 0x00008000u, 0x00000000u, 0x80003800u, 0x00003800u}}, + {{0x80000001u, 0x00400000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0xffffff6cu, 0x00000000u, 0xb6a00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u}}, + {{0x3e800000u, 0x80800000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0x00000000u, 0x3fd00000u, 0x00002000u, 0x00004000u, 0x00000000u, 0x80003400u, 0x00003400u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0x00000000u, 0x3fd00000u, 0x00002000u, 0x00004000u, 0x00000000u, 0x80003400u, 0x00003400u}}, + {{0xbf800000u, 0x00000001u}, {0xffffffffu, 0x00000000u, 0xffffffffu, 0xffffffffu, 0x00000001u, 0x00000000u, 0xbff00000u, 0x00008001u, 0x00000000u, 0x00000000u, 0x0000bc00u, 0x0000bc00u}, {0xffffffffu, 0x00000000u, 0xffffffffu, 0xffffffffu, 0x00000001u, 0x00000000u, 0xbff00000u, 0x00008001u, 0x00000000u, 0x00000000u, 0x0000bc00u, 0x0000bc00u}}, +}}; + +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; +} + +bool InPass(std::uint32_t column, bool float64) { + return (column == Float64Low || column == Float64High) == float64; +} + +std::vector Run(AgcDriver::VulkanDevice& device, bool float64, ShaderRecompiler::ShaderFloatMode floatMode) { + Input.fill(0u); + for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) { + Input[lane * Inputs] = Vectors[lane].operands[0]; + Input[lane * Inputs + 1u] = Vectors[lane].operands[1]; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const auto code = float64 ? std::span(Float64Code) : std::span(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {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; + request.context.floatMode = floatMode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + return {Output.begin(), Output.end()}; +} + +std::string Where(std::uint32_t lane, std::uint32_t column, std::uint32_t denormals, bool ieee) { + return std::string(Names[column]) + " of " + Hex(Vectors[lane].operands[0]) + ", " + Hex(Vectors[lane].operands[1]) + " in f32 denormal mode " + + std::to_string(denormals) + (ieee ? ", IEEE=1" : ", IEEE=0"); +} + +void CheckFlushed(const std::vector& output, bool float64, std::uint32_t denormals, bool ieee) { + for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) { + for (std::uint32_t column = 0; column < Columns; ++column) { + if (!InPass(column, float64)) continue; + const auto value = output[lane * Results + column]; + const auto expected = Vectors[lane].denormalsFlushed[column]; + Require(value == expected, Where(lane, column, denormals, ieee) + " is " + Hex(value) + ", expected " + Hex(expected)); + } + } +} + +void CheckKept(const std::vector& output, bool float64, bool ieee) { + for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) { + for (std::uint32_t column = 0; column < Columns; ++column) { + if (!InPass(column, float64)) continue; + const auto value = output[lane * Results + column]; + const auto& vector = Vectors[lane]; + Require(value == vector.denormalsKept[column] || value == vector.denormalsFlushed[column], + Where(lane, column, 3u, ieee) + " is " + Hex(value) + ", expected " + Hex(vector.denormalsKept[column]) + " or, on a driver that flushes f32 denormals, " + + Hex(vector.denormalsFlushed[column])); + } + } +} + +void CheckSame(const std::vector& output, const std::vector& reference, bool float64, std::uint32_t denormals, std::uint32_t like, bool ieee) { + for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) { + for (std::uint32_t column = 0; column < Columns; ++column) { + if (!InPass(column, float64)) continue; + const auto value = output[lane * Results + column]; + const auto expected = reference[lane * Results + column]; + Require(value == expected, Where(lane, column, denormals, ieee) + " is " + Hex(value) + ", but mode " + std::to_string(like) + " gave " + Hex(expected)); + } + } +} + +ShaderRecompiler::ShaderFloatMode Mode(std::uint32_t denormals, bool ieee) { + return ShaderRecompiler::ShaderFloatMode{0xc0u | denormals << 4u, true, ieee, false}; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const bool hasFloat64 = TargetHasCapability(device->Target(), spv::CapabilityFloat64); + if (!hasFloat64) std::puts("v_cvt_f64_f32 skipped, the device has no shaderFloat64"); + for (const bool float64 : {false, true}) { + if (float64 && !hasFloat64) continue; + for (const bool ieee : {false, true}) { + const auto flushed = Run(*device, float64, Mode(0u, ieee)); + CheckFlushed(flushed, float64, 0u, ieee); + const auto inputsFlushed = Run(*device, float64, Mode(2u, ieee)); + CheckFlushed(inputsFlushed, float64, 2u, ieee); + CheckSame(inputsFlushed, flushed, float64, 2u, 0u, ieee); + const auto kept = Run(*device, float64, Mode(3u, ieee)); + CheckKept(kept, float64, ieee); + const auto outputsFlushed = Run(*device, float64, Mode(1u, ieee)); + CheckSame(outputsFlushed, kept, float64, 1u, 3u, ieee); + } + } + std::puts("f32 conversion denormal mode 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/Float16Dot.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Dot.cpp index 961afeb7c9..0a96011da8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Float16Dot.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Dot.cpp @@ -31,6 +31,12 @@ alignas(256) constexpr std::array Code{ 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, 0xbf810000, }; +alignas(256) constexpr std::array AccumulatorCode{ + 0x34020084, 0x34060086, 0xe0381000, 0x80000401, 0xbf8c3f70, 0xcc13600a, 0x1c1a0b04, 0xcc13000b, + 0x1c1a0b04, 0xcc13440c, 0x1c1a0b04, 0xcc13400d, 0x9c1a0b04, 0xcc13440e, 0x9c1a0b04, 0xe0781000, + 0x80010a03, 0xe0701010, 0x80010e03, 0xbf810000, +}; + constexpr std::uint32_t Rows[32][4] = { {0x0001bc00u, 0x81bfacbau, 0xc541aa2fu, 0x00000000u}, {0x01f61a79u, 0x3c000381u, 0xc1729fffu, 0x00000000u}, @@ -101,6 +107,43 @@ constexpr std::uint32_t Expected[32][10] = { {0xbaf43c38u, 0xbaf43c38u, 0x3af43c38u, 0x3af43c38u, 0x3abe63ecu, 0xbaf43c38u, 0xc021e000u, 0x3fb50000u, 0xba90cd60u, 0x3abe63ecu} }; +constexpr std::uint32_t AccumulatorExpected[32][5] = { + {0xc541a901u, 0xc541a901u, 0x4541ab5du, 0x4541ab5du, 0xc541a901u}, + {0xc1729fdfu, 0xc1729fdfu, 0x4172a01fu, 0x4172a01fu, 0xc1729fdfu}, + {0x4187e315u, 0x4187e315u, 0x4187e315u, 0xc1882737u, 0xc1882737u}, + {0x3f7fdb0fu, 0x3f7fdb0fu, 0x3f7fdb0fu, 0xbf801279u, 0xbf801279u}, + {0xbe511687u, 0xbe511687u, 0xbe511687u, 0xbe5166f4u, 0xbe5166f4u}, + {0xc1e56f13u, 0xc1e56f13u, 0x41e55ba5u, 0x41e55ba5u, 0xc1e56f13u}, + {0x439a1228u, 0x439a1228u, 0x439a1228u, 0xc3a5d2acu, 0xc3a5d2acu}, + {0x37f5fb14u, 0x37f5fb14u, 0x37f5fc0cu, 0x37f5fc0cu, 0x37f5fb14u}, + {0x7fd0a000u, 0x7fd0a000u, 0x7fd0a000u, 0x7fd0a000u, 0x7fd0a000u}, + {0xffc8ab86u, 0xffc8ab86u, 0x7fc8ab86u, 0x7fc8ab86u, 0xffc8ab86u}, + {0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u}, + {0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u}, + {0xb974e480u, 0xb974e480u, 0xb974e480u, 0xb974e480u, 0xb974e480u}, + {0x7f800000u, 0x7f800000u, 0x7f800000u, 0xff800000u, 0xff800000u}, + {0xff800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0xff800000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u}, + {0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u}, + {0x7fc42000u, 0x7fc42000u, 0x7fc42000u, 0x7fc42000u, 0x7fc42000u}, + {0xbf052ae0u, 0xbf052ae0u, 0xbf052ae0u, 0xbf052ae0u, 0xbf052ae0u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x5f834da7u, 0x5f834da7u, 0x5f834da7u, 0xdf834da7u, 0xdf834da7u}, + {0x7f7fffffu, 0x7f7fffffu, 0x7f7fffffu, 0xff7fffffu, 0xff7fffffu}, + {0x42750eeeu, 0x42750eeeu, 0x42750eeeu, 0xc2750eeeu, 0xc2750eeeu}, + {0x43380748u, 0x43380748u, 0x43380748u, 0xc3380746u, 0xc3380746u}, + {0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u}, + {0xf540cddbu, 0xf540cddbu, 0x7540cddbu, 0x7540cddbu, 0xf540cddbu}, + {0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u}, + {0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0xffe00000u, 0xffe00000u}, + {0x3cb7c720u, 0x3cb7c720u, 0x3cb7c720u, 0x3cb7c720u, 0x3cb7c720u}, + {0xbf9bd158u, 0xbf9bd158u, 0xbf9bd158u, 0xbf9bd158u, 0xbf9bd158u}, + {0x565f49ebu, 0x565f49ebu, 0x565f49ebu, 0xd65f49ebu, 0xd65f49ebu}, + {0xbaf43c38u, 0xbaf43c38u, 0xbaf43c38u, 0xbaf43c38u, 0xbaf43c38u} +}; + +constexpr const char* AccumulatorNames[5] = {"v_dot2 op_sel src2", "v_dot2 op_sel_hi src2 cleared", "v_dot2 neg_hi src2", "v_dot2 neg_lo src2", "v_dot2 neg_lo and neg_hi src2"}; + constexpr const char* Names[10] = {"v_dot2c", "v_dot2", "v_dot2 neg_lo src0/src2 neg_hi src1", "v_dot2 neg_lo src1 neg_hi src0", "v_dot2 op_sel src0", "v_dot2 clamp", "v_dot2c inline 1.0", "v_dot2c literal", "v_dot2c dpp quad_perm", "v_dot2 op_sel src1"}; void Fill(std::uint32_t tid, std::uint32_t* words) { @@ -140,21 +183,9 @@ auto Compile(AgcDriver::VulkanDevice& device, std::span cod return ShaderRecompiler::Recompile(request); } -void CheckRefused(AgcDriver::VulkanDevice& device, std::uint32_t word0, const std::string& what) { - alignas(256) const std::array code{word0, 0x1c1a0b04u, 0xbf810000u}; - std::string refusal; - try { - static_cast(Compile(device, code)); - } catch (const std::exception& error) { - refusal = error.what(); - } - Require(refusal.find("v_dot2_f32_f16 accumulator op_sel, op_sel_hi and neg_hi are not implemented") != std::string::npos, what + " was not refused"); -} - -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(); @@ -167,6 +198,13 @@ void Check() { } } +void CheckAccumulator() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t* out = &Output[tid * Results]; + for (std::uint32_t i = 0; i < 5; ++i) Expect(tid, out[i], AccumulatorExpected[tid][i], AccumulatorNames[i]); + } +} + } int main() { @@ -177,11 +215,10 @@ int main() { std::puts("skipped, v_dot2_f32_f16 is computed in f64 and the device has no shaderFloat64"); return VulkanTestSkipped; } - Run(*device); + Run(*device, Code); Check(); - CheckRefused(*device, 0xcc13600au, "op_sel on the accumulator"); - CheckRefused(*device, 0xcc13440au, "neg_hi on the accumulator"); - CheckRefused(*device, 0xcc13000au, "op_sel_hi cleared on the accumulator"); + Run(*device, AccumulatorCode); + CheckAccumulator(); std::puts("v_dot2_f32_f16 tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp index f840ade501..bb50f103fd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp @@ -37,7 +37,9 @@ constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t UnormFormat = 30; constexpr std::uint32_t UintFormat = 34; constexpr std::uint32_t FloatFormat = 43; +constexpr std::uint32_t NativeFloatFormat = 36; constexpr std::size_t FloatOffset = 40960; +constexpr std::size_t Float36Offset = 45056; constexpr std::uint32_t SwizzleXYZ1 = 0x3acu; constexpr std::uint32_t SwizzleXZY1 = 0x374u; constexpr std::uint32_t SwizzleZYX1 = 0x32eu; @@ -285,6 +287,16 @@ constexpr std::array, 7> FloatStoreEdges{{ {0x7f800001u, 0x7f810000u, 0x7fa00000u, 0xfa1f07e1u}, }}; +constexpr std::array, 7> Float36StoreEdges{{ + {0xbf800000u, 0x80000000u, 0x7fc00000u, 0xfc000000u}, + {0x7f800000u, 0x7149f2cau, 0x477fff00u, 0xf7fdffc0u}, + {0x3f830000u, 0x3f818000u, 0x35800000u, 0x001e03c1u}, + {0x00000001u, 0xff800000u, 0xffc00000u, 0xfc000000u}, + {0x477c0000u, 0x4788b800u, 0x33000000u, 0x003dffbeu}, + {0x36400000u, 0x477e0000u, 0x3dcccccdu, 0x5cfdf803u}, + {0x7f800001u, 0x7f810000u, 0x7fa00000u, 0xfa3e0fc1u}, +}}; + constexpr std::array, 3> FloatLoadEdges{{ {0x3e0u | (0x7c0u << 10u) | (0x03fu << 21u), 0x7f800000u, 0x7f800000u, 0x387c0000u}, {0x001u | (0x041u << 10u) | (0x7bfu << 21u), 0x36000000u, 0x38820000u, 0x477e0000u}, @@ -302,6 +314,10 @@ std::uint32_t FloatTexel(std::uint32_t index) { return SmallFloat(FloatValues[index % 11u], 5u) | (SmallFloat(FloatValues[(index + 4u) % 11u], 6u) << 10u) | (SmallFloat(FloatValues[(index + 8u) % 11u], 6u) << 21u); } +std::uint32_t Float36Texel(std::uint32_t index) { + return SmallFloat(FloatValues[index % 11u], 6u) | (SmallFloat(FloatValues[(index + 4u) % 11u], 6u) << 11u) | (SmallFloat(FloatValues[(index + 8u) % 11u], 5u) << 22u); +} + void CheckFloatLoad(AgcDriver::VulkanDevice& device, std::uint8_t* texels) { for (std::uint32_t index = 0; index < LoadWidth; ++index) { const auto texel = index < FloatLoadEdges.size() ? FloatLoadEdges[index][0] : FloatTexel(index); @@ -344,8 +360,32 @@ void CheckFloatStore(AgcDriver::VulkanDevice& device, std::uint8_t* texels) { } } -void CheckFloatSample(AgcDriver::VulkanDevice& device, const std::uint8_t* texels, const std::array& expected, const std::string& what) { - Run(device, SampleLz, TextureDescriptor(texels, FloatFormat, SwizzleXYZ1, Threads), PointSampler, 1); +void CheckFloat36Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels) { + std::fill(texels, texels + Threads * 4u, static_cast(0xa5u)); + Input.fill(0u); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * 4u + 0u] = std::bit_cast(FloatValues[tid % 11u]); + Input[tid * 4u + 1u] = std::bit_cast(FloatValues[(tid + 4u) % 11u]); + Input[tid * 4u + 2u] = std::bit_cast(FloatValues[(tid + 8u) % 11u]); + if (tid < Float36StoreEdges.size()) { + for (std::uint32_t component = 0; component < 3u; ++component) { + Input[tid * 4u + component] = Float36StoreEdges[tid][component]; + } + } + } + Run(device, StoreXyz, TextureDescriptor(texels, NativeFloatFormat, SwizzleXYZ1, Threads), BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)), 1); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), Threads * 4u, nullptr, "test"); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + std::uint32_t actual = 0; + std::memcpy(&actual, texels + tid * 4u, 4u); + const auto expected = tid < Float36StoreEdges.size() ? Float36StoreEdges[tid][3] : Float36Texel(tid); + Require(actual == expected, "image_store of R11_G11_B10_FLOAT: texel " + std::to_string(tid) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } +} + +void CheckFloatSample(AgcDriver::VulkanDevice& device, const std::uint8_t* texels, const std::array& expected, const std::string& what, std::uint32_t format = FloatFormat) { + Run(device, SampleLz, TextureDescriptor(texels, format, SwizzleXYZ1, Threads), PointSampler, 1); for (std::uint32_t tid = 0; tid < Threads; ++tid) { for (std::uint32_t component = 0; component < 4u; ++component) { const auto actual = Output[tid * 4u + component]; @@ -545,6 +585,9 @@ int main(int argc, char** argv) { CheckFloatSample(*device, texels + FloatOffset, {0u, 0u, 0x7fc00000u, 0x3f800000u}, "image_sample_lz of R10_G11_B11_FLOAT results still on the GPU"); CheckFloatLoad(*device, texels + FloatOffset); CheckFloatSample(*device, texels + FloatOffset, {0x7f800000u, 0x7f800000u, 0x387c0000u, 0x3f800000u}, "image_sample_lz of R10_G11_B11_FLOAT"); + CheckFloat36Store(*device, texels + Float36Offset); + Run(*device, StoreXyz, TextureDescriptor(texels + Float36Offset, NativeFloatFormat, SwizzleXYZ1, Threads), BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)), 1); + CheckFloatSample(*device, texels + Float36Offset, {0u, 0u, 0x7fc00000u, 0x3f800000u}, "image_sample_lz of R11_G11_B10_FLOAT results still on the GPU", NativeFloatFormat); CheckSample(*device, texels, SampleAt, SampleStep, SampleStart, SwizzleXYZ1, PointSampler, false, false, "point image_sample_lz X Y Z 1"); CheckSample(*device, texels, SampleAt, SampleStep, SampleStart, SwizzleZYX1, BilinearSampler, true, false, "bilinear image_sample_lz Z Y X 1"); CheckSample(*device, texels, SampleWrapped, SampleStep, WrappedStart, SwizzleX011, WrapPointSampler, false, true, "wrapped point image_sample_lz X 0 1 1"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreLineThroughPlaneArray.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreLineThroughPlaneArray.cpp new file mode 100644 index 0000000000..26d3e4d1ff --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreLineThroughPlaneArray.cpp @@ -0,0 +1,185 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Width = 32; +constexpr std::uint32_t Type1D = 8; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::size_t TexelBytes = 4096; +constexpr std::size_t BlockBytes = 65536; + +alignas(256) std::array Input{}; + +alignas(256) constexpr std::array StoreCode{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0281, 0x7e400280, 0xf0201f28, 0x0001141e, + 0x7e3e0280, 0x7e4002c1, 0xf0201528, 0x0001141e, 0x7e400281, 0xf0201528, 0x0001141e, 0xbf810000, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "image store line through plane array: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +struct Format { + const char* name; + std::uint32_t format; + std::uint32_t components; +}; + +constexpr std::array Formats{{ + {"32_SINT", 21u, 1u}, + {"32_32_32_32_SINT", 76u, 4u}, +}}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t Value(std::uint32_t tid, std::uint32_t index) { + return (tid * 4u + index + 1u) * 0x9e3779b1u; +} + +std::uint64_t TexelOffset(const Format& format, std::uint32_t x) { + const auto mip = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, format.format, Width, 1u, 1u).at(0); + return mip.tiledOffset + static_cast(x) * format.components * 4u; +} + +std::vector Initial() { + std::vector bytes(TexelBytes); + for (std::size_t index = 0; index < bytes.size(); ++index) bytes[index] = static_cast(0x80u + 37u * index + 11u * (index >> 4u)); + return bytes; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array 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, + IdentitySwizzle | (Type1D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::span code, std::uint32_t format) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t index = 0; index < 4u; ++index) Input[tid * 4u + index] = Value(tid, index); + } + const auto initial = Initial(); + std::copy(initial.begin(), initial.end(), texels); + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto texture = TextureDescriptor(texels, format); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), TexelBytes, nullptr, "test"); + device.WaitIdle(); +} + +void CheckTexels(const std::uint8_t* texels, const Format& format) { + const auto initial = Initial(); + for (std::uint32_t x = 0; x < Width; ++x) { + const auto offset = TexelOffset(format, x); + for (std::uint32_t component = 0; component < format.components; ++component) { + std::uint32_t actual = 0; + std::memcpy(&actual, texels + offset + component * 4u, 4u); + const auto wanted = Value(x, component); + Require(actual == wanted, std::string("image_store 2d_array through a 1D ") + format.name + " T#: texel " + std::to_string(x) + " component " + std::to_string(component) + " is " + Hex(actual) + ", expected the y = 1, slice = 0 store " + Hex(wanted) + " and the stores to slices 0xffffffff and 1 dropped"); + } + } + const auto end = TexelOffset(format, Width); + Require(std::equal(texels + end, texels + TexelBytes, initial.begin() + static_cast(end)), std::string("image_store 2d_array through a 1D ") + format.name + " T#: bytes past the texture changed"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock block; + auto* texels = block.Data(); + for (const auto& format : Formats) { + Dispatch(*device, texels, StoreCode, format.format); + CheckTexels(texels, format); + } + std::puts("image store line through plane array tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index d5a89a1586..5ee8abca21 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -47,6 +47,7 @@ constexpr std::uint32_t MaxGroups = 8; constexpr std::uint32_t Format32UInt = 20; constexpr std::uint32_t Format32SInt = 21; constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t Format32_32UInt = 62; constexpr std::uint32_t Format11_11_10UInt = 34; constexpr std::uint32_t Format8_8Srgb = 129; constexpr std::uint32_t Type2D = 9; @@ -56,6 +57,7 @@ constexpr std::uint32_t DepthSwizzleMode = 0x18; alignas(256) std::array FloatTexels{}; alignas(256) std::array OtherFloatTexels{}; alignas(256) std::array UintTexels{}; +alignas(256) std::array Uint64Texels{}; alignas(256) std::array SintTexels{}; alignas(4096) std::array DepthStorage{}; alignas(256) std::array Table{}; @@ -208,6 +210,12 @@ alignas(256) constexpr std::array AtomicAddLaneTableCode{ 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7e080301u, 0xf0448108u, 0x00090401u, 0xbf810000u, }; +alignas(256) constexpr std::array AtomicAdd64LaneTableCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, + 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7e080301u, 0x7e0a02ffu, 0x00000000u, 0xf0448308u, + 0x00090401u, 0xbf810000u, +}; + alignas(256) constexpr std::array AtomicSwapLaneTableCode{ 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7e080301u, 0xf03c8108u, 0x00090401u, 0xbf810000u, @@ -1168,6 +1176,32 @@ void RunAtomicTests(AgcDriver::VulkanDevice& device) { for (std::uint32_t x = 0; x < Width; ++x) Require(UintTexels[x] == (x ^ 1u) && std::bit_cast(FloatTexels[x]) == 0x3f800000u + x, "image table atomic: an add through a null entry reached an image:\n" + null.log); } +void RunAtomic64Tests(AgcDriver::VulkanDevice& device) { + if (!TargetHasCapability(device.Target(), spv::CapabilityInt64ImageEXT)) { + std::puts("skipped the 64-bit table atomic rows, the device has no shaderImageInt64Atomics"); + return; + } + struct Registration { + void* data; + Registration(void* data, std::size_t bytes) : data(data) { GuestAllocations::Mutation().Add(data, bytes, true, true, true); } + ~Registration() { GuestAllocations::Mutation().Remove(data); } + } registered(Uint64Texels.data(), sizeof(Uint64Texels)); + SetClassRecords(); + for (std::uint32_t x = 0; x < Width; ++x) Uint64Texels[x] = 0x5000000000000000ull + x; + SetRecord(9, PointClamp, Image(Uint64Texels.data(), Format32_32UInt)); + Keys = {}; + Keys[0] = 9; + const auto added = Run(device, AtomicAdd64LaneTableCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table atomic64 test"); + Require(!Faulted(added), "image table atomic64: an add through a table faulted:\n" + added.log); + Require(added.imageTableCount != 0u, "image table atomic64: the add bound no storage table elements"); + for (std::uint32_t x = 0; x < Width; ++x) Require(Uint64Texels[x] == 0x5000000000000000ull + x + x, "image table atomic64: texel " + std::to_string(x) + " is wrong"); + Keys[0] = 6; + const auto null = Run(device, AtomicAdd64LaneTableCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table atomic64 test"); + for (std::uint32_t x = 0; x < Width; ++x) Require(Uint64Texels[x] == 0x5000000000000000ull + x + x, "image table atomic64: an add through a null entry reached an image:\n" + null.log); +} + void RunSelectedStoreTests(AgcDriver::VulkanDevice& device) { alignas(256) static std::array storeTexels{}; struct Registration { @@ -1561,6 +1595,7 @@ int main() { RunModeTests(*device); RunStoreTests(*device); RunAtomicTests(*device); + RunAtomic64Tests(*device); RunSelectedStoreTests(*device); RunPointerTests(*device); RunPointerRecordTests(*device); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp index dd5588f04c..0e677dd20c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp @@ -1,9 +1,12 @@ +#include "prx/libSceAgcDriver/libSceAgcDriver.hpp" +#include "prx/libSceAgc/libSceAgc.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libkernel/include/Api.hpp" -#include "prx/libSceJobManager/include/Api.hpp" +#include "prx/libSceJobManager/libSceJobManager.hpp" #include "VulkanTestDevice.hpp" #include "SceTypes.hpp" -#include "prx/libc/include/GuestArena.hpp" #include +#include "prx/libc/include/GuestArena.hpp" #include #include #include @@ -17,7 +20,6 @@ #include #include -extern "C" void AgcDriverShutdown_nid_postfix(); #define Construct _ZN3sce3Job10JobManagerC1Ev #define RequiredSize _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE @@ -82,6 +84,7 @@ std::uint64_t SizeFor(std::uint32_t computeSequences, std::uint64_t mask, bool l std::atomic Released{false}; std::atomic SeenByJob{0}; +std::atomic SecondSeenByJob{0}; void APS5_VABI BlockingJob(void*, std::uint32_t) { while (!Released) std::this_thread::yield(); @@ -91,6 +94,12 @@ void APS5_VABI ReadOutputJob(void*, std::uint32_t) { SeenByJob = Output[0]; } +void APS5_VABI ReadBothOutputsJob(void* argument, std::uint32_t) { + SeenByJob = Output[0]; + SecondSeenByJob = Output[1]; + std::atomic_ref(*static_cast(argument)).store(1); +} + void APS5_VABI Nothing(void*, std::uint32_t) { } @@ -230,6 +239,256 @@ void CapacityCase() { ::operator delete(memory, std::align_val_t{256}); } +void DeferredInputsCase(bool mixed) { + for (const bool indirect : {false, true}) { + const auto size = SizeFor(1, 1, true); + auto* memory = static_cast(::operator new(size, std::align_val_t{256})); + std::memset(memory, 0, size); + alignas(64) unsigned char manager[0x100]{}; + Construct(manager); + Require(Initialize(manager, memory, size, 2, 1, 1, 1, nullptr) == 0, "snapshot manager initialization failed"); + auto* factory = Factories(manager, 0); + ISequence* sequence = nullptr; + Require(factory->CreateCompute(&sequence, 1, 3, 1, 1, nullptr) == 0, "snapshot sequence creation failed"); + auto registers = Registers; + auto shader = Shader; + const ShaderRegister* pointer = registers.data(); + std::memcpy(shader.data() + 0x20, &pointer, sizeof(pointer)); + alignas(16) const std::array redirected{Low(Output + 1), High(Output + 1) & 0xffffu, sizeof(Output[1]), 0x31016facu}; + const std::string expected = indirect ? "snapshot indirect" : "snapshot direct"; + std::array marker{}; + std::memcpy(marker.data(), expected.c_str(), expected.size() + 1); + Released = false; + Output[0] = 0; + Output[1] = 0; + Value[0] = indirect ? 0x51a70002u : 0x51a70001u; + if (mixed) Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == 0, "snapshot parallel compute dispatch failed"); + Require(sequence->Dispatch(BlockingJob, nullptr, 1, nullptr) == 0, "snapshot blocking job failed"); + Require(sequence->SyncPoint() == 0, "snapshot sync point failed"); + const int result = indirect ? sequence->DispatchComputeIndirect(shader.data(), Table.data(), IndirectArguments.data(), marker.data()) + : sequence->DispatchCompute(shader.data(), Table.data(), 1, 1, 1, marker.data()); + Require(result == 0, "snapshot dispatch failed"); + const bool encoded = std::search(memory, memory + size, expected.begin(), expected.end()) != memory + size; + registers[7].value = Low(redirected.data()); + registers[8].value = High(redirected.data()); + std::memset(marker.data(), 'x', expected.size()); + Released = true; + Require(sequence->Wait() == 0, "snapshot wait failed"); + const bool updated = Output[0] == (mixed ? Value[0] : 0) && Output[1] == Value[0]; + const bool markerUpdated = std::search(memory, memory + size, marker.begin(), marker.begin() + expected.size()) != memory + size; + Require(factory->DestroyCompute(sequence) == 0, "snapshot destruction failed"); + Require(Shutdown(manager) == 0, "snapshot shutdown failed"); + ::operator delete(memory, std::align_val_t{256}); + Require(!encoded, "compute commands were encoded before their CPU barrier retired"); + Require(updated, "queued compute commands did not read their deferred shader registers"); + Require(markerUpdated, "queued compute commands did not read their deferred marker"); + } +} + +void GpuBarriersCase() { + for (const bool indirect : {false, true}) { + const auto size = SizeFor(1, 2, true); + auto* memory = static_cast(::operator new(size, std::align_val_t{256})); + std::memset(memory, 0, size); + alignas(64) unsigned char manager[0x100]{}; + Construct(manager); + Require(Initialize(manager, memory, size, 2, 1, 1, 2, nullptr) == 0, "GPU barrier initialization failed"); + auto* factory = Factories(manager, 0); + ISequence* sequence = nullptr; + Require(factory->CreateCompute(&sequence, 1, 8, 4, 2, nullptr) == 0, "GPU barrier sequence creation failed"); + auto registers = Registers; + auto shader = Shader; + const ShaderRegister* pointer = registers.data(); + std::memcpy(shader.data() + 0x20, &pointer, sizeof(pointer)); + alignas(16) const std::array redirected{Low(Output + 1), High(Output + 1) & 0xffffu, sizeof(Output[1]), 0x31016facu}; + alignas(64) std::uint32_t gate = 0; + alignas(256) std::array commands{}; + CommandBuffer buffer{commands.data(), commands.data() + commands.size(), commands.data(), commands.data() + commands.size(), nullptr, nullptr, 0}; + Require(sceAgcAcbWaitRegMem(&buffer, 0, 3, 3, &gate, 1, 0xffffffffu, 0x190) != nullptr, "GPU barrier gate encoding failed"); + const Packet packet{commands.data(), static_cast(buffer.cursor_up - commands.data()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x21, &packet) == 0, "GPU barrier gate submission failed"); + Output[0] = 0; + Output[1] = 0; + Value[0] = indirect ? 0x51a80002u : 0x51a80001u; + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, "first") == 0, "GPU barrier first dispatch failed"); + Require(sequence->SyncPoint() == 0, "GPU barrier sync point failed"); + const std::string expected = indirect ? "GPU barrier indirect" : "GPU barrier direct"; + std::array marker{}; + std::memcpy(marker.data(), expected.c_str(), expected.size() + 1); + const int result = indirect ? sequence->DispatchComputeIndirect(shader.data(), Table.data(), IndirectArguments.data(), marker.data()) + : sequence->DispatchCompute(shader.data(), Table.data(), 1, 1, 1, marker.data()); + Require(result == 0, "GPU barrier second dispatch failed"); + const bool encoded = std::search(memory, memory + size, expected.begin(), expected.end()) != memory + size; + registers[7].value = Low(redirected.data()); + registers[8].value = High(redirected.data()); + std::memset(marker.data(), 'x', expected.size()); + std::atomic_ref(gate).store(1); + Require(sequence->Wait() == 0, "GPU barrier wait failed"); + Require(encoded, "GPU-only sync deferred the following dispatch inputs"); + Require(Output[0] == Value[0] && Output[1] == 0, "GPU-only sync did not capture shader registers before dispatch returned"); + alignas(16) const std::array dependentTable{Low(Output), High(Output)}; + for (std::uint32_t round = 0; round < 3; ++round) { + Output[0] = 0; + Output[1] = 0; + Value[0] = 0x52a80000u + round; + std::atomic_ref(gate).store(0); + Require(sceAgcDriverSubmitAcb(0x21, &packet) == 0, "GPU dependency gate submission failed"); + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == 0, "GPU dependency producer failed"); + Require(sequence->SyncPoint() == 0, "GPU dependency sync failed"); + Require(sequence->DispatchCompute(shader.data(), const_cast(dependentTable.data()), 1, 1, 1, nullptr) == 0, "GPU dependency consumer failed"); + std::atomic_ref(gate).store(1); + Require(sequence->Wait() == 0, "GPU dependency wait failed"); + Require(Output[0] == Value[0] && Output[1] == Value[0], "GPU sync did not make producer writes visible to its consumer"); + } + if (!indirect) { + alignas(64) std::uint32_t overlapGate = 0; + alignas(256) std::array overlapCommands{}; + CommandBuffer overlapBuffer{overlapCommands.data(), overlapCommands.data() + overlapCommands.size(), overlapCommands.data(), overlapCommands.data() + overlapCommands.size(), nullptr, nullptr, 0}; + Require(sceAgcAcbWaitRegMem(&overlapBuffer, 0, 3, 3, &overlapGate, 1, 0xffffffffu, 0x190) != nullptr, "GPU overlap gate encoding failed"); + const Packet overlapPacket{overlapCommands.data(), static_cast(overlapBuffer.cursor_up - overlapCommands.data()), 0, {}}; + Output[0] = 0; + Output[1] = 0; + std::atomic_ref(gate).store(0); + Require(sceAgcDriverSubmitAcb(0x21, &packet) == 0, "GPU overlap gate submission failed"); + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == 0, "GPU overlap producer failed"); + Require(sequence->SyncPoint() == 0, "GPU overlap sync failed"); + Require(sceAgcDriverSubmitAcb(0x21, &overlapPacket) == 0, "GPU overlap gate submission failed"); + Require(sequence->DispatchCompute(shader.data(), Table.data(), 1, 1, 1, nullptr) == 0, "GPU overlap consumer failed"); + Require(sequence->Dispatch(ReadBothOutputsJob, &overlapGate, 1, nullptr) == 0, "GPU overlap CPU callback failed"); + std::atomic missedDeadline{false}; + std::jthread rescue([&](std::stop_token stop) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + const std::atomic_ref signal(overlapGate); + while (!stop.stop_requested() && signal.load() == 0 && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + if (signal.load() == 0) { + missedDeadline = true; + signal.store(1); + } + }); + std::atomic_ref(gate).store(1); + Require(sequence->Wait() == 0, "GPU overlap wait failed"); + rescue.request_stop(); + rescue.join(); + Require(!missedDeadline, "CPU work did not release GPU work beyond its preceding sync"); + Require(SeenByJob == Value[0] && SecondSeenByJob == 0, "CPU work waited for GPU work beyond its preceding sync"); + Require(Output[1] != 0, "GPU overlap consumer did not finish before Wait returned"); + alignas(16) const std::array originalTarget{Low(Output + 2), High(Output + 2) & 0xffffu, sizeof(Output[2]), 0x31016facu}; + alignas(16) const std::array changedTarget{Low(Output + 3), High(Output + 3) & 0xffffu, sizeof(Output[3]), 0x31016facu}; + Output[2] = 0; + Output[3] = 0; + registers[7].value = Low(originalTarget.data()); + registers[8].value = High(originalTarget.data()); + const std::string afterWait = "after waiting caller"; + std::memcpy(marker.data(), afterWait.c_str(), afterWait.size() + 1); + std::atomic_ref(gate).store(0); + Require(sceAgcDriverSubmitAcb(0x21, &packet) == 0, "GPU wait-boundary gate submission failed"); + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == 0, "GPU wait-boundary producer failed"); + Require(sequence->SyncPoint() == 0, "GPU wait-boundary sync failed"); + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == 0, "GPU wait-boundary second dispatch failed"); + std::atomic waiting{false}; + std::thread waiter([&] { + waiting.store(true); + Require(sequence->Wait() == 0, "GPU wait-boundary first wait failed"); + }); + while (!waiting.load()) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(sequence->DispatchCompute(shader.data(), Table.data(), 1, 1, 1, marker.data()) == 0, "GPU dispatch after a queued wait failed"); + const bool crossedWait = std::search(memory, memory + size, afterWait.begin(), afterWait.end()) != memory + size; + registers[7].value = Low(changedTarget.data()); + registers[8].value = High(changedTarget.data()); + std::atomic_ref(gate).store(1); + waiter.join(); + Require(sequence->Wait() == 0, "GPU wait-boundary final wait failed"); + Require(!crossedWait && Output[2] == 0 && Output[3] == Value[0], "compute lookahead crossed a waiting caller's boundary"); + } + Require(factory->DestroyCompute(sequence) == 0, "GPU barrier destruction failed"); + Require(Shutdown(manager) == 0, "GPU barrier shutdown failed"); + ::operator delete(memory, std::align_val_t{256}); + } +} + +void ComputeShutdownCase() { + for (const std::uint32_t mode : {0u, 1u, 2u}) { + const bool waiting = mode == 1; + const bool queuedCpu = mode == 2; + const auto size = SizeFor(1, 2, true); + const auto allocationSize = (size + 255) & ~std::uint64_t{255}; + auto* memory = static_cast(::operator new(allocationSize, std::align_val_t{256})); + std::memset(memory, 0, size); + std::memset(memory + size, 0xa5, allocationSize - size); + alignas(64) unsigned char manager[0x100]{}; + Construct(manager); + Require(Initialize(manager, memory, size, 2, 1, 1, 2, nullptr) == 0, "compute shutdown initialization failed"); + auto* factory = Factories(manager, 0); + ISequence* sequence = nullptr; + Require(factory->CreateCompute(&sequence, 1, 4, 2, 2, nullptr) == 0, "compute shutdown sequence creation failed"); + auto registers = Registers; + auto shader = Shader; + alignas(16) const std::array descriptor{Low(Output + 1), High(Output + 1) & 0xffffu, sizeof(Output[1]), 0x31016facu}; + registers[7].value = Low(descriptor.data()); + registers[8].value = High(descriptor.data()); + const ShaderRegister* registerPointer = registers.data(); + std::memcpy(shader.data() + 0x20, ®isterPointer, sizeof(registerPointer)); + alignas(64) std::uint32_t gate = 0; + alignas(256) std::array commands{}; + CommandBuffer buffer{commands.data(), commands.data() + commands.size(), commands.data(), commands.data() + commands.size(), nullptr, nullptr, 0}; + Require(sceAgcAcbWaitRegMem(&buffer, 0, 3, 3, &gate, 1, 0xffffffffu, 0x190) != nullptr, "compute shutdown gate encoding failed"); + const Packet packet{commands.data(), static_cast(buffer.cursor_up - commands.data()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x21, &packet) == 0, "compute shutdown gate submission failed"); + Output[0] = 0; + Output[1] = 0; + SeenByJob = 0; + Value[0] = 0x5a170000u + mode; + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == 0, "compute shutdown first dispatch failed"); + Require(sequence->SyncPoint() == 0, "compute shutdown first sync failed"); + if (queuedCpu) { + Require(sequence->Dispatch(ReadOutputJob, nullptr, 1, nullptr) == 0, "compute shutdown queued CPU dispatch failed"); + Require(sequence->SyncPoint() == 0, "compute shutdown second sync failed"); + } + Require(sequence->DispatchCompute(shader.data(), Table.data(), 1, 1, 1, nullptr) == 0, "compute shutdown second dispatch failed"); + std::atomic waitingStarted{false}; + std::atomic waitingReturned{false}; + std::thread waiter; + if (waiting) { + waiter = std::thread([&] { + waitingStarted.store(true); + Require(sequence->Wait() == 0, "compute shutdown canceled wait failed"); + waitingReturned.store(true); + }); + while (!waitingStarted.load()) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(!waitingReturned.load(), "compute shutdown wait passed the held GPU gate"); + } + std::atomic stopped{false}; + std::thread stopper([&] { + Require(Shutdown(manager) == 0, "compute shutdown failed"); + stopped.store(true); + }); + const auto stopDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (!stopped.load() && std::chrono::steady_clock::now() < stopDeadline) std::this_thread::yield(); + const bool stoppedBeforeGpu = stopped.load(); + while (waiting && !waitingReturned.load() && std::chrono::steady_clock::now() < stopDeadline) std::this_thread::yield(); + const bool waiterBeforeGpu = !waiting || waitingReturned.load(); + std::atomic_ref(gate).store(1); + stopper.join(); + if (waiting) waiter.join(); + alignas(256) Label fence{}; + alignas(256) std::array tailWords{}; + CommandBuffer tail{tailWords.data(), tailWords.data() + tailWords.size(), tailWords.data(), tailWords.data() + tailWords.size(), nullptr, nullptr, 0}; + Require(sceAgcCbReleaseMem(&tail, 0x2f, 0x200, 1, 3, &fence, 1, 1, 0, 0, 0, 0) != nullptr, "compute shutdown tail fence encoding failed"); + const Packet tailPacket{tailWords.data(), static_cast(tail.cursor_up - tailWords.data()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x21, &tailPacket) == 0, "compute shutdown tail fence submission failed"); + const auto gpuDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (fence.value != 1 && std::chrono::steady_clock::now() < gpuDeadline) std::this_thread::yield(); + Require(fence.value == 1, "submitted compute work did not survive manager shutdown"); + Require(stoppedBeforeGpu && waiterBeforeGpu, "shutdown waited for GPU completion"); + Require(Output[0] == Value[0] && Output[1] == (queuedCpu ? 0 : Value[0]), "shutdown changed submitted or deferred compute work"); + Require(SeenByJob == 0, "shutdown published queued CPU work after GPU completion"); + for (auto offset = size; offset < allocationSize; ++offset) Require(memory[offset] == 0xa5, "compute shutdown wrote past the caller work area"); + ::operator delete(memory, std::align_val_t{256}); + } +} + void StorageCase(bool directMemory) { constexpr std::uint64_t mask = (1u << 1) | (1u << 5) | (1u << 8); const MemorySizeQueryParams query{1, 2, 0, 0, 32, 8, 4, 2, mask}; @@ -251,9 +510,21 @@ void StorageCase(bool directMemory) { alignas(64) unsigned char manager[0x100]{}; Construct(manager); Require(Initialize(manager, memory, size, 2, 0, 2, mask, nullptr) == 0, "storage manager initialization failed"); + const auto inside = [memory, size](const void* pointer) { + const auto base = reinterpret_cast(memory); + const auto address = reinterpret_cast(pointer); + return address >= base && address < base + size; + }; + void* implementation = nullptr; + std::memcpy(&implementation, manager, sizeof(implementation)); + Require(inside(implementation), "manager state is outside the supplied work area"); auto* factory = Factories(manager, 0); + Require(inside(factory), "compute factory is outside the supplied work area"); std::array sequences{}; - for (auto& sequence : sequences) Require(factory->CreateCompute(&sequence, 0, 16, 4, 2, nullptr) == 0, "storage sequence creation failed"); + for (auto& sequence : sequences) { + Require(factory->CreateCompute(&sequence, 0, 16, 4, 2, nullptr) == 0, "storage sequence creation failed"); + Require(inside(sequence), "compute sequence is outside the supplied work area"); + } for (std::uint32_t round = 0; round < 3; ++round) { for (auto* sequence : sequences) { Value[0] = 0x56780000u + round; @@ -294,7 +565,7 @@ void StorageCase(bool directMemory) { int RunJobManagerCase(const char* name) { try { - Require(std::strcmp(name, "compute") == 0 || std::strcmp(name, "size") == 0 || std::strcmp(name, "execution") == 0 || std::strcmp(name, "storage") == 0 || std::strcmp(name, "capacity") == 0, "unknown compute fixture"); + Require(std::strcmp(name, "compute") == 0 || std::strcmp(name, "size") == 0 || std::strcmp(name, "execution") == 0 || std::strcmp(name, "storage") == 0 || std::strcmp(name, "capacity") == 0 || std::strcmp(name, "deferred_inputs") == 0 || std::strcmp(name, "gpu_barriers") == 0 || std::strcmp(name, "compute_shutdown") == 0, "unknown compute fixture"); #ifndef ANYPS5_JOB_MANAGER_REFERENCE auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; @@ -304,6 +575,12 @@ int RunJobManagerCase(const char* name) { if (std::strcmp(name, "compute") == 0 || std::strcmp(name, "size") == 0) SizeCase(); if (std::strcmp(name, "compute") == 0 || std::strcmp(name, "execution") == 0) RunCase(); if (std::strcmp(name, "compute") == 0 || std::strcmp(name, "capacity") == 0) CapacityCase(); + if (std::strcmp(name, "compute") == 0 || std::strcmp(name, "deferred_inputs") == 0) { + DeferredInputsCase(false); + DeferredInputsCase(true); + } + if (std::strcmp(name, "compute") == 0 || std::strcmp(name, "gpu_barriers") == 0) GpuBarriersCase(); + if (std::strcmp(name, "compute") == 0 || std::strcmp(name, "compute_shutdown") == 0) ComputeShutdownCase(); if (std::strcmp(name, "compute") == 0 || std::strcmp(name, "storage") == 0) { StorageCase(false); StorageCase(true); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCompletionPoints.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCompletionPoints.cpp new file mode 100644 index 0000000000..8c8bd50ef9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCompletionPoints.cpp @@ -0,0 +1,395 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" +#include "prx/libSceAgcDriver/Eq/include/Event.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "prx/libkernel/Equeue/Equeue.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 +#include +#include +#include +#include +#include +#include +#include + + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 128; +constexpr std::size_t Pitch = std::size_t{Width} * 4u; +constexpr std::size_t TargetBytes = Pitch * Height; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t LabelOffset = 3 * BlockBytes; +constexpr std::uint32_t Untouched = 0x5a5a5a5au; +constexpr std::uint32_t Drawn = 0xff0000ffu; +constexpr std::uint32_t LabelValue = 0x600dcafeu; +constexpr std::uint32_t OutputHandle = 9; +constexpr std::uint32_t Rgba8UnormInfo = 0x28028u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::array Scissor{32, 16, 160, 80}; + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060208u, 0x7e0802f2u, 0x7e0a02ffu, 0x44800000u, 0xf80000d1u, 0x00000005u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array FillCode{ + 0xd7460004, 0x04010c08, 0x7e000204, 0x7e020205, 0x7e040206, 0x7e060207, 0xe01c2000, 0x80000004, 0xbf810000, +}; + +alignas(4) std::atomic Gate = 0; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +void AppendDraw(std::vector& commands, std::uint64_t target) { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const auto extent = (Height << 16u) | Width; + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0x01200000}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, Rgba8UnormInfo}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, ((Width - 1u) << 14u) | (Height - 1u)}, + {0x3b8, LinearAttrib3}, {0x1e0, 0}, + {0xc, 0}, {0xd, extent}, + {0x81, 0x80000000}, {0x82, extent}, + {0x90, 0x80000000u | (Scissor[1] << 16u) | Scissor[0]}, {0x91, (Scissor[3] << 16u) | Scissor[2]}, + {0x94, 0x80000000}, {0x95, extent}, + {0x10f, std::bit_cast(2.0f * Width)}, {0x110, std::bit_cast(2.0f * Width)}, + {0x111, std::bit_cast(2.0f * Height)}, {0x112, std::bit_cast(2.0f * Height)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + {0x318, static_cast(target >> 8u)}, {0x390, static_cast(target >> 40u)}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + Append(commands, 0x79, {0x242, 4}); + Append(commands, 0x79, {0x24a, 0}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + Append(commands, 0x2d, {3, 2}); +} + +void AppendGate(std::vector& commands) { + const auto gate = reinterpret_cast(&Gate); + commands.insert(commands.end(), {0xc0053c00u, 0x13u, static_cast(gate), static_cast(gate >> 32u), 1u, 0xffffffffu, 0x19u}); +} + +void Submit(std::vector& commands) { + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); +} + +class Region { +public: + Region(std::size_t bytes, bool watched) : bytes(bytes), arena(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (arena) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + if (data != nullptr) GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (arena) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "linear target completion points: cannot map the guest region"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "linear target completion points: cannot allocate the guest region"); + auto* words = reinterpret_cast(data); + std::fill(words, words + bytes / 4u, Untouched); + if (arena && !watched) { + AgcDriver::GuestMemory::Unwatch(Address(), bytes); + Require(!AgcDriver::GuestMemory::Watched(Address(), bytes), "linear target completion points: the region is still write-watched"); + } + GuestAllocations::Mutation().Add(data, bytes, true, true, true); + } + ~Region() { + GuestAllocations::Mutation().Remove(data); +#ifdef _WIN32 + if (arena) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (arena) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Region(const Region&) = delete; + Region& operator=(const Region&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::uint32_t Word(std::size_t offset) const { return std::atomic_ref(*reinterpret_cast(data + offset)).load(std::memory_order_acquire); } + std::size_t bytes; + +private: + bool arena; + std::uint8_t* data = nullptr; +}; + +void Import(const Region& region) { + const auto address = region.Address(); + const std::vector userData{ + static_cast(address), + static_cast((address >> 32u) & 0xffffu) | (16u << 16u), + static_cast(region.bytes / 16u), + 0x4bfacu, + Untouched, + Untouched, + Untouched, + Untouched, + }; + std::vector commands; + const auto code = reinterpret_cast(FillCode.data()); + Append(commands, 0x76, {0x207, 64, 1, 1}); + Append(commands, 0x76, {0x20c, static_cast(code >> 8u), static_cast(code >> 40u)}); + Append(commands, 0x76, {0x213, (static_cast(userData.size()) << 1u) | 0x80u}); + commands.push_back(0xc0007600u | (static_cast(userData.size()) << 16u)); + commands.push_back(0x240u); + commands.insert(commands.end(), userData.begin(), userData.end()); + commands.insert(commands.end(), {0xc0031500u, static_cast(region.bytes / 1024u), 1, 1, 0x41u}); + Submit(commands); + AgcDriverWaitIdle_nid_postfix(); +} + +bool InsideScissor(std::uint32_t x, std::uint32_t y) { + return x >= Scissor[0] && x < Scissor[2] && y >= Scissor[1] && y < Scissor[3]; +} + +std::string Mismatches(const Region& region) { + std::size_t drawn = 0, outside = 0; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto value = region.Word(y * Pitch + x * 4u); + if (InsideScissor(x, y)) drawn += value == Drawn ? 0u : 1u; + else outside += value == Untouched ? 0u : 1u; + } + } + if (drawn == 0 && outside == 0) return {}; + return std::to_string(drawn) + " of the " + std::to_string((Scissor[2] - Scissor[0]) * (Scissor[3] - Scissor[1])) + " pixels inside the scissor do not hold the draw's color and " + std::to_string(outside) + " pixels outside it changed"; +} + +bool Await(const std::function& done) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!done()) { + if (std::chrono::steady_clock::now() >= deadline) return false; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return true; +} + +class Request final : public AgcDriver::IFlipRequest { +public: + explicit Request(std::atomic& ready) : ready(ready) {} + void GpuReady(const std::shared_ptr&) override { ++ready; } + void Fail(std::exception_ptr) noexcept override {} +private: + std::atomic& ready; +}; + +class Output final : public AgcDriver::IVideoOutput { +public: + std::shared_ptr Reserve(const AgcDriver::FlipInfo&) override { return std::make_shared(ready); } + void Fail(std::exception_ptr) noexcept override {} + std::atomic ready = 0; +}; + +enum class Point { Flip, Label, Interrupt }; + +struct Events { + KernelEqueue queue = 0; + KernelEqueueRef owner; + int tag = 0; +}; + +std::string Run(Point point, bool watched, const Region& region, Output& output, Events& events) { + const char* name = point == Point::Flip ? "flip" : point == Point::Label ? "label" : "end-of-pipe interrupt"; + const auto target = region.Address(); + Import(region); + Gate.store(0, std::memory_order_release); + std::vector commands; + AppendDraw(commands, target); + if (point == Point::Flip) { + commands.insert(commands.end(), {AgcDriver::FlipPacketHeader, OutputHandle, 0xfffffffeu, 1, 0, 0}); + } else if (point == Point::Label) { + const auto label = target + LabelOffset; + commands.insert(commands.end(), {0xc0064900u, 0x514u, 1u << 29u, static_cast(label), static_cast(label >> 32u), LabelValue, 0, 0}); + } else { + commands.insert(commands.end(), {0xc0064900u, 0x06000528u, 2u << 24u, 0, 0, 0, 0, 0}); + } + AppendGate(commands); + const auto readyBefore = output.ready.load(); + if (point == Point::Interrupt) { + std::array stale{}; + while (events.owner->GetTriggeredEvents(stale.data(), static_cast(stale.size())) > 0) {} + } + Submit(commands); + const bool watchedAtStore = AgcDriver::GuestMemory::Watched(target, TargetBytes); + std::string failure; + if (point == Point::Flip) { + const bool flipped = Await([&] { return output.ready.load() != readyBefore; }); + if (!flipped) failure = "the flip did not reach the GPU"; + else if (!Await([&] { return Mismatches(region).empty(); })) failure = "after the flip, while the queue waited: " + Mismatches(region); + } else if (point == Point::Label) { + if (!Await([&] { return region.Word(LabelOffset) == LabelValue; })) failure = "the label after the draw never landed"; + else if (const auto mismatches = Mismatches(region); !mismatches.empty()) failure = "when the label after the draw landed: " + mismatches; + } else { + std::array delivered{}; + if (!Await([&] { return events.owner->GetTriggeredEvents(delivered.data(), 1) == 1; })) failure = "the end-of-pipe interrupt after the draw was not delivered"; + else if (delivered[0].udata != &events.tag) failure = "an unexpected event was delivered"; + else if (const auto mismatches = Mismatches(region); !mismatches.empty()) failure = "when the end-of-pipe interrupt after the draw was delivered: " + mismatches; + } + Gate.store(1, std::memory_order_release); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::Graphics::StorageTexture::FlushPending(target, region.bytes, nullptr, "test release"); + std::printf("linear target completion point: %s, %s memory (watched at the store: %d): %s\n", name, watched ? "write-watched" : "unwatched", watchedAtStore ? 1 : 0, failure.empty() ? "stored" : failure.c_str()); + if (failure.empty()) return {}; + return std::string(name) + (watched ? " (write-watched memory): " : " (unwatched memory): ") + failure; +} + +} + +int main(int argc, char** argv) { + try { + bool flips = argc == 1, labels = argc == 1, interrupts = argc == 1, watchedFlip = argc == 1; + for (int index = 1; index < argc; ++index) { + const std::string_view argument(argv[index]); + if (argument == "--flip") flips = true; + else if (argument == "--label") labels = true; + else if (argument == "--interrupt") interrupts = true; + else Require(false, "linear target completion points: unknown argument " + std::string(argument)); + } + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).hostImportAlignment == 0) { + std::puts("linear target completion point tests skipped: no host import on this device, so the target is never resident"); + return VulkanTestSkipped; + } + } + std::vector> regions; + struct ShutdownGuard { + ~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} } + } shutdown; + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + auto output = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(OutputHandle, output); + Events events; + if (interrupts) { + Require(sceKernelCreateEqueue(&events.queue, "AGC linear target") == 0, "linear target completion points: event queue creation failed"); + events.owner = EqueuePin_nid_postfix(events.queue); + Require(sceAgcDriverAddEqEvent(events.queue, 0, &events.tag) == 0, "linear target completion points: graphics event registration failed"); + } + std::vector failures; + const auto run = [&](Point point, bool watched) { + regions.push_back(std::make_unique(watched ? 16 * BlockBytes : 4 * BlockBytes, watched)); + if (auto failure = Run(point, watched, *regions.back(), *output, events); !failure.empty()) failures.push_back(std::move(failure)); + }; + if (flips) run(Point::Flip, false); + if (watchedFlip) run(Point::Flip, true); + if (labels) run(Point::Label, false); + if (interrupts) run(Point::Interrupt, false); + if (interrupts) { + Require(sceAgcDriverDeleteEqEvent(events.queue, 0) == 0, "linear target completion points: graphics event deletion failed"); + events.owner.reset(); + Require(sceKernelDeleteEqueue(events.queue) == 0, "linear target completion points: event queue deletion failed"); + } + AgcDriverUnregisterVideoOutput_nid_postfix(OutputHandle, output); + if (!failures.empty()) { + std::string message = "a CPU read after a point where the guest can observe a draw's completion did not see the drawn linear target:"; + for (const auto& failure : failures) message += "\n " + failure; + Require(false, message); + } + AgcDriverShutdown_nid_postfix(); + std::puts("linear target completion point 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/LinearTargetCompletionStores.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCompletionStores.cpp new file mode 100644 index 0000000000..e6abff0879 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCompletionStores.cpp @@ -0,0 +1,304 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.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 + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t TargetBytes = std::size_t{Width} * Height * 16u; +constexpr std::size_t LabelOffset = BlockBytes; +constexpr std::uint32_t Rgba32FloatInfo = 0x8738u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32AbgrExport = 9; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 2> Programs{}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::span Program(std::size_t index, bool blue) { + auto& code = Programs.at(index); + std::size_t at = 0; + code[at++] = 0x36000000u | (20u << 17u) | 0xffu; + code[at++] = 0x0000ffffu; + code[at++] = 0x2c000000u | (21u << 17u) | 0x90u; + code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u); + code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u); + code[at++] = (blue ? 0x7e0002f2u : 0x7e000280u) | (22u << 17u); + code[at++] = 0x7e0002f2u | (23u << 17u); + code[at++] = 0xf800180fu; + code[at++] = 0x17161514u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Rgba32FloatInfo; + cx[0x31d] = 0; + cx[0x323] = 0; + cx[0x324] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = LinearAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + color.exportIndex = 0; + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::span pixelCode, VkRect2D scissor) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.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 vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionFixedPoint); + pixel.targetOutputMode.at(0) = Float32AbgrExport; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + VkPipelineColorBlendAttachmentState blend{}; + blend.blendEnable = VK_FALSE; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = scissor; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::array Texel(const std::uint8_t* target, std::uint32_t x, std::uint32_t y) { + std::array texel{}; + std::memcpy(texel.data(), target + (static_cast(y) * Width + x) * 16u, sizeof(texel)); + return texel; +} + +void Label(AgcDriver::VulkanDevice& device, const Block& block, std::uint32_t value, std::uint64_t submissions, bool watched, const char* what) { + const auto bytes = std::as_bytes(std::span(&value, 1)); + const auto reason = device.WriteLabelOnGpu(block.Address() + LabelOffset, bytes, value, 0); + Require(reason == 0, std::string(what) + ": the label was not recorded on the GPU (outcome " + std::to_string(reason) + ")"); + Require(StorageTexture::FindPending(block.Address(), TargetBytes) == nullptr, std::string(what) + ": the drawn linear target was still pending after a label the GPU writes"); + const auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(!watched || (recorder != nullptr && recorder->Submissions() == submissions), std::string(what) + ": storing the drawn linear target before the label waited for recorded work (" + std::to_string(recorder != nullptr ? recorder->Submissions() - submissions : 0) + " submissions)"); +} + +bool Expect(AgcDriver::VulkanDevice& device) { + Block block(16 * BlockBytes); + std::fill(block.data, block.data + block.bytes, std::uint8_t{0xff}); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) return false; + const auto color = DecodeTarget(block); + Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && color.bytes == TargetBytes, "the target is not a linear R32G32B32A32 target"); + const auto red = Program(0, false); + const auto blue = Program(1, true); + const auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr, "the device has no recorder"); + const bool watched = AgcDriver::GuestMemory::Watched(block.Address(), TargetBytes); + if (!watched) std::puts("linear target completion stores: the target is outside the write watch, so a store may wait for the previous store's saved bytes; the submission checks are skipped"); + const auto pending = [&](const char* what) { + Require(StorageTexture::FindPending(block.Address(), TargetBytes) != nullptr, std::string(what) + ": the drawn linear target was stored before any point where its completion is observable"); + }; + Draw(device, color, red, {{0, 0}, {Width, Height}}); + pending("after the first draw"); + const auto submissions = recorder->Submissions(); + Draw(device, color, blue, {{0, 0}, {Width / 2u, Height}}); + pending("after the second draw"); + Require(!watched || recorder->Submissions() == submissions, "the second draw into the linear target submitted recorded work"); + Draw(device, color, red, {{Width / 4u, Height / 4u}, {Width / 4u, Height / 2u}}); + pending("after the third draw"); + Require(!watched || recorder->Submissions() == submissions, "the third draw into the linear target submitted recorded work"); + Label(device, block, 1u, submissions, watched, "the first label"); + Draw(device, color, blue, {{Width / 2u, 0}, {Width / 2u, Height}}); + pending("after the fourth draw"); + Label(device, block, 2u, submissions, watched, "the second label"); + Draw(device, color, red, {{0, Height / 2u}, {Width, Height / 2u}}); + pending("after the fifth draw"); + Require(!watched || recorder->Submissions() == submissions, "the fifth draw into the linear target submitted recorded work"); + device.WaitIdle(); + Require(StorageTexture::FindPending(block.Address(), TargetBytes) == nullptr, "the drawn linear target was still pending after the device wait"); + std::vector stored(TargetBytes); + std::memcpy(stored.data(), block.data, TargetBytes); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const bool lastRed = y >= Height / 2u; + const bool lastBlue = !lastRed && x >= Width / 2u; + const bool thirdRed = !lastRed && !lastBlue && x >= Width / 4u && x < Width / 2u && y >= Height / 4u; + const bool secondBlue = !lastRed && !lastBlue && !thirdRed; + const float z = lastRed || thirdRed ? 0.0f : secondBlue || lastBlue ? 1.0f : -1.0f; + const std::array expected{static_cast(x), static_cast(y), z, 1.0f}; + const auto texel = Texel(stored.data(), x, y); + Require(texel == expected, "after the device wait, raw guest read: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + ")"); + } + } + std::uint32_t label = 0; + std::memcpy(&label, block.data + LabelOffset, sizeof(label)); + Require(label == 2u, "the second label did not land: it holds " + std::to_string(label)); + return true; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!Expect(*device)) { + std::puts("linear target completion store test skipped: no host import on this device"); + return VulkanTestSkipped; + } + std::puts("linear target completion store 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/LinearTargetCpuWrites.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCpuWrites.cpp new file mode 100644 index 0000000000..cbd2e525e4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCpuWrites.cpp @@ -0,0 +1,310 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.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 + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t TargetBytes = std::size_t{Width} * Height * 16u; +constexpr std::size_t RowBytes = std::size_t{Width} * 16u; +constexpr std::uint32_t Drawn = Width / 2u; +constexpr std::uint32_t Rgba32FloatInfo = 0x8738u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32AbgrExport = 9; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 2> Programs{}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::span Program(std::size_t index, bool blue) { + auto& code = Programs.at(index); + std::size_t at = 0; + code[at++] = 0x36000000u | (20u << 17u) | 0xffu; + code[at++] = 0x0000ffffu; + code[at++] = 0x2c000000u | (21u << 17u) | 0x90u; + code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u); + code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u); + code[at++] = (blue ? 0x7e0002f2u : 0x7e000280u) | (22u << 17u); + code[at++] = 0x7e0002f2u | (23u << 17u); + code[at++] = 0xf800180fu; + code[at++] = 0x17161514u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + Block(std::size_t bytes, bool stayWatched) : bytes(bytes), arena(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (arena) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (arena) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!arena || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + if (arena && !stayWatched) { + AgcDriver::GuestMemory::Unwatch(Address(), bytes); + Require(!AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is still write-watched"); + } + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (arena) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (arena) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool arena; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Rgba32FloatInfo; + cx[0x31d] = 0; + cx[0x323] = 0; + cx[0x324] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = LinearAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + color.exportIndex = 0; + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::span pixelCode, std::uint32_t scissorWidth) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.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 vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionFixedPoint); + pixel.targetOutputMode.at(0) = Float32AbgrExport; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + VkPipelineColorBlendAttachmentState blend{}; + blend.blendEnable = VK_FALSE; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {scissorWidth, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::array Texel(const std::uint8_t* target, std::uint32_t x, std::uint32_t y) { + std::array texel{}; + std::memcpy(texel.data(), target + static_cast(y) * RowBytes + static_cast(x) * 16u, sizeof(texel)); + return texel; +} + +enum class Outcome { Kept, Lost, Skipped }; + +Outcome Run(AgcDriver::VulkanDevice& device, bool watched, bool seeded, bool hookRead) { + const auto name = std::string(seeded ? "seeded" : "unseeded") + ", " + (watched ? "write-watched" : "unwatched") + " memory" + (hookRead ? ", driver read before the store" : ""); + Block block(watched ? std::size_t{16} * BlockBytes : BlockBytes, watched); + std::fill(block.data, block.data + block.bytes, std::uint8_t{0xff}); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) return Outcome::Skipped; + const auto color = DecodeTarget(block); + Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && color.bytes == TargetBytes, "the target is not a linear R32G32B32A32 target"); + Draw(device, color, Program(0, false), Width); + const auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr && !recorder->Idle(), "the draw into an imported linear target waited for the GPU instead of being recorded"); + device.WaitIdle(); + std::vector stored(TargetBytes); + std::memcpy(stored.data(), block.data, TargetBytes); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const std::array expected{static_cast(x), static_cast(y), 0.0f, 1.0f}; + const auto texel = Texel(stored.data(), x, y); + Require(texel == expected, name + ": after the first draw, raw guest read: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + ")"); + } + } + if (seeded) { + for (std::uint32_t y = 0; y < Height; ++y) std::memset(block.data + y * RowBytes, 0x5a, Drawn * 16u); + } + Draw(device, color, Program(1, true), Drawn); + for (std::uint32_t y = 0; y < Height; ++y) std::memset(block.data + y * RowBytes + Drawn * 16u, 0x42, RowBytes - Drawn * 16u); + const bool watchedAtStore = AgcDriver::GuestMemory::Watched(block.Address(), TargetBytes); + if (hookRead) { + std::vector row(RowBytes); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(row)), 1); + for (std::uint32_t x = 0; x < Drawn; ++x) { + const std::array expected{static_cast(x), 0.0f, 1.0f, 1.0f}; + const auto texel = Texel(row.data(), x, 0); + Require(texel == expected, name + ": a driver read of row 0 before the store: pixel (" + std::to_string(x) + ", 0) holds (" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + "), not the second draw's"); + } + } + device.WaitIdle(); + std::size_t wrongGpu = 0; + std::size_t wrongCpu = 0; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Drawn; ++x) { + const std::array expected{static_cast(x), static_cast(y), 1.0f, 1.0f}; + if (Texel(block.data, x, y) != expected) ++wrongGpu; + } + auto* begin = block.data + y * RowBytes + Drawn * 16u; + wrongCpu += static_cast(std::count_if(begin, block.data + (y + 1u) * RowBytes, [](std::uint8_t byte) { return byte != 0x42u; })); + } + std::printf("linear target CPU writes, %s (watched at the store: %d): wrong GPU pixels %zu/%u, wrong CPU bytes %zu/%zu\n", name.c_str(), watchedAtStore ? 1 : 0, wrongGpu, Drawn * Height, wrongCpu, (RowBytes - Drawn * 16u) * Height); + return wrongGpu == 0 && wrongCpu == 0 ? Outcome::Kept : Outcome::Lost; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + bool kept = true; + bool ran = false; + for (const bool watched : {true, false}) { + for (const bool seeded : {false, true}) { + for (const bool hookRead : {false, true}) { + const auto outcome = Run(*device, watched, seeded, hookRead); + ran = ran || outcome != Outcome::Skipped; + kept = kept && outcome != Outcome::Lost; + } + } + } + if (!ran) { + std::puts("linear target CPU write test skipped: no host import on this device"); + return VulkanTestSkipped; + } + Require(kept, "a drawn linear target's store lost the drawn half or the CPU's later stores to the other half"); + std::puts("linear target CPU write 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/LinearTargetDirectWriteCompletion.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDirectWriteCompletion.cpp new file mode 100644 index 0000000000..4a10c86def --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDirectWriteCompletion.cpp @@ -0,0 +1,452 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.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 +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 128; +constexpr std::size_t Pitch = std::size_t{Width} * 4u; +constexpr std::size_t TargetBytes = Pitch * Height; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t RegionBytes = 16 * BlockBytes; +constexpr std::size_t LabelOffset = 3 * BlockBytes; +constexpr std::uint32_t Untouched = 0x5a5a5a5au; +constexpr std::uint32_t Filled = 0x9abcdef0u; +constexpr std::uint32_t Drawn = 0xff0000ffu; +constexpr std::uint32_t LabelValue = 0x600dcafeu; +constexpr std::uint32_t CpuStored = 0x0badf00du; +constexpr std::uint32_t Rgba8UnormInfo = 0x28028u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t WaitEqual = 3; +constexpr std::uint32_t WaitAtLeast = 5; +constexpr std::array, 2> Scissors{{{16, 8, 128, 96}, {128, 32, 240, 120}}}; +constexpr std::array BlockZeroScissor{24, 8, 232, 56}; +constexpr std::size_t CpuStoreOffset = BlockBytes + 32 * Pitch + 64 * 4; +static_assert(BlockZeroScissor[3] * Pitch <= BlockBytes); +static_assert(CpuStoreOffset >= BlockBytes && CpuStoreOffset < TargetBytes); + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060208u, 0x7e0802f2u, 0x7e0a02ffu, 0x44800000u, 0xf80000d1u, 0x00000005u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array FillCode{ + 0xd7460004, 0x04010c08, 0x7e000204, 0x7e020205, 0x7e040206, 0x7e060207, 0xe01c2000, 0x80000004, 0xbf810000, +}; +alignas(256) constexpr std::array PairFillCode{ + 0xd7460004, 0x04010c0c, 0x7e000204, 0x7e020205, 0x7e040206, 0x7e060207, 0xe01c2000, 0x80000004, 0xe01c2000, 0x80020004, 0xbf810000, +}; + +alignas(4) std::atomic Gate = 0; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +void AppendDraw(std::vector& commands, std::uint64_t target, const std::array& scissor) { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const auto extent = (Height << 16u) | Width; + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0x01200000}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, Rgba8UnormInfo}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, ((Width - 1u) << 14u) | (Height - 1u)}, + {0x3b8, LinearAttrib3}, {0x1e0, 0}, + {0xc, 0}, {0xd, extent}, + {0x81, 0x80000000}, {0x82, extent}, + {0x90, 0x80000000u | (scissor[1] << 16u) | scissor[0]}, {0x91, (scissor[3] << 16u) | scissor[2]}, + {0x94, 0x80000000}, {0x95, extent}, + {0x10f, std::bit_cast(2.0f * Width)}, {0x110, std::bit_cast(2.0f * Width)}, + {0x111, std::bit_cast(2.0f * Height)}, {0x112, std::bit_cast(2.0f * Height)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + {0x318, static_cast(target >> 8u)}, {0x390, static_cast(target >> 40u)}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + Append(commands, 0x79, {0x242, 4}); + Append(commands, 0x79, {0x24a, 0}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + Append(commands, 0x2d, {3, 2}); +} + +std::array BufferDescriptor(std::uint64_t address, std::size_t bytes) { + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(bytes / 16u), 0x4bfacu}; +} + +void AppendDispatch(std::vector& commands, const std::uint32_t* program, const std::vector& userData, std::uint32_t groups) { + const auto code = reinterpret_cast(program); + Append(commands, 0x76, {0x207, 64, 1, 1}); + Append(commands, 0x76, {0x20c, static_cast(code >> 8u), static_cast(code >> 40u)}); + Append(commands, 0x76, {0x213, (static_cast(userData.size()) << 1u) | 0x80u}); + commands.push_back(0xc0007600u | (static_cast(userData.size()) << 16u)); + commands.push_back(0x240u); + commands.insert(commands.end(), userData.begin(), userData.end()); + commands.insert(commands.end(), {0xc0031500u, groups, 1, 1, 0x41u}); +} + +void AppendFill(std::vector& commands, std::uint64_t target, const std::uint32_t* copied, std::size_t bytes) { + const auto direct = BufferDescriptor(target, bytes); + const auto other = BufferDescriptor(reinterpret_cast(copied), bytes); + std::vector userData(direct.begin(), direct.end()); + userData.insert(userData.end(), {Filled, Filled, Filled, Filled}); + userData.insert(userData.end(), other.begin(), other.end()); + AppendDispatch(commands, PairFillCode.data(), userData, static_cast(bytes / 1024u)); +} + +void AppendLabel(std::vector& commands, std::uint64_t label) { + commands.insert(commands.end(), {0xc0064900u, 0x514u, 1u << 29u, static_cast(label), static_cast(label >> 32u), LabelValue, 0, 0}); +} + +void AppendGate(std::vector& commands, std::uint32_t function, std::uint32_t reference) { + const auto gate = reinterpret_cast(&Gate); + commands.insert(commands.end(), {0xc0053c00u, 0x10u | function, static_cast(gate), static_cast(gate >> 32u), reference, 0xffffffffu, 0x19u}); +} + +void Submit(std::vector& commands) { + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); +} + +class Region { +public: + explicit Region(std::size_t bytes) : bytes(bytes), arena(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (arena) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + if (data != nullptr) GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (arena) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "linear target direct write completion: cannot map the guest region"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "linear target direct write completion: cannot allocate the guest region"); + auto* words = reinterpret_cast(data); + std::fill(words, words + bytes / 4u, Untouched); + GuestAllocations::Mutation().Add(data, bytes, true, true, true); + } + ~Region() { + GuestAllocations::Mutation().Remove(data); +#ifdef _WIN32 + if (arena) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (arena) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Region(const Region&) = delete; + Region& operator=(const Region&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::uint32_t Word(std::size_t offset) const { return std::atomic_ref(*reinterpret_cast(data + offset)).load(std::memory_order_acquire); } + void Put(std::size_t offset, std::uint32_t value) const { std::atomic_ref(*reinterpret_cast(data + offset)).store(value, std::memory_order_release); } + std::size_t bytes; + +private: + bool arena; + std::uint8_t* data = nullptr; +}; + +void Import(const Region& region) { + const auto buffer = BufferDescriptor(region.Address(), region.bytes); + std::vector userData(buffer.begin(), buffer.end()); + userData.insert(userData.end(), {Untouched, Untouched, Untouched, Untouched}); + std::vector commands; + AppendDispatch(commands, FillCode.data(), userData, static_cast(region.bytes / 1024u)); + Submit(commands); + AgcDriverWaitIdle_nid_postfix(); +} + +bool Covered(std::uint32_t x, std::uint32_t y) { + return std::any_of(Scissors.begin(), Scissors.end(), [&](const std::array& scissor) { return x >= scissor[0] && x < scissor[2] && y >= scissor[1] && y < scissor[3]; }); +} + +std::string Mismatches(const Region& region, bool drawsLast) { + std::size_t covered = 0, wrongCovered = 0, filledCovered = 0, wrongElsewhere = 0, drawnElsewhere = 0; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto value = region.Word(y * Pitch + x * 4u); + if (drawsLast && Covered(x, y)) { + ++covered; + wrongCovered += value == Drawn ? 0u : 1u; + filledCovered += value == Filled ? 1u : 0u; + } else { + wrongElsewhere += value == Filled ? 0u : 1u; + drawnElsewhere += value == Drawn ? 1u : 0u; + } + } + } + if (wrongCovered == 0 && wrongElsewhere == 0) return {}; + const auto elsewhere = std::to_string(std::size_t{Width} * Height - covered); + if (!drawsLast) return std::to_string(wrongElsewhere) + " of the " + elsewhere + " pixels do not hold the fill's pattern (" + std::to_string(drawnElsewhere) + " hold the draws' color)"; + return std::to_string(wrongCovered) + " of the " + std::to_string(covered) + " pixels the draws covered do not hold their color (" + std::to_string(filledCovered) + " hold the fill's pattern) and " + std::to_string(wrongElsewhere) + " of the " + elsewhere + " pixels outside them do not hold the fill's pattern"; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::string CpuStoreMismatches(const Region& region) { + std::size_t covered = 0, wrongCovered = 0, filledCovered = 0, others = 0, wrongOthers = 0; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto offset = y * Pitch + x * 4u; + if (offset == CpuStoreOffset) continue; + const auto value = region.Word(offset); + if (x >= BlockZeroScissor[0] && x < BlockZeroScissor[2] && y >= BlockZeroScissor[1] && y < BlockZeroScissor[3]) { + ++covered; + wrongCovered += value == Drawn ? 0u : 1u; + filledCovered += value == Filled ? 1u : 0u; + } else { + ++others; + wrongOthers += value == (offset < BlockBytes ? Filled : Untouched) ? 0u : 1u; + } + } + } + std::vector parts; + if (wrongCovered != 0) parts.push_back(std::to_string(wrongCovered) + " of the " + std::to_string(covered) + " pixels the draw covered in block 0 do not hold its color (" + std::to_string(filledCovered) + " hold the fill's pattern)"); + if (const auto stored = region.Word(CpuStoreOffset); stored != CpuStored) parts.push_back("the CPU store in block 1 holds " + Hex(stored) + ", not " + Hex(CpuStored)); + if (wrongOthers != 0) parts.push_back(std::to_string(wrongOthers) + " of the " + std::to_string(others) + " other pixels do not hold the fill's pattern in block 0 or the untouched pattern in block 1"); + std::string text; + for (const auto& part : parts) text += (text.empty() ? "" : "; ") + part; + return text; +} + +bool Await(const std::function& done) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!done()) { + if (std::chrono::steady_clock::now() >= deadline) return false; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return true; +} + +enum class Order { FillFirst, DrawsFirst }; + +bool Run(Order order, const Region& region, std::vector& failures) { + const bool fillFirst = order == Order::FillFirst; + const char* name = fillFirst ? "fill, then draws" : "draws, then fill"; + const auto target = region.Address(); + Import(region); + if (!AgcDriver::GuestMemory::Watched(target, TargetBytes)) return false; + std::vector copied(TargetBytes / 4u + 4u, Untouched); + GuestAllocations::Mutation().Add(copied.data(), TargetBytes + 16u, true, true, true); + struct Unregister { + void* data; + ~Unregister() { GuestAllocations::Mutation().Remove(data); } + } unregister{copied.data()}; + Gate.store(0, std::memory_order_release); + std::vector commands; + if (fillFirst) AppendFill(commands, target, copied.data(), TargetBytes); + for (const auto& scissor : Scissors) AppendDraw(commands, target, scissor); + if (!fillFirst) AppendFill(commands, target, copied.data(), TargetBytes); + AppendLabel(commands, target + LabelOffset); + if (fillFirst) AppendGate(commands, WaitEqual, 1); + Submit(commands); + std::string failure; + if (fillFirst) { + if (!Await([&] { return region.Word(LabelOffset) == LabelValue; })) failure = "the label after the draws never landed"; + else if (const auto mismatches = Mismatches(region, true); !mismatches.empty()) failure = "when the label after the draws landed: " + mismatches; + Gate.store(1, std::memory_order_release); + AgcDriverWaitIdle_nid_postfix(); + } else { + AgcDriverWaitIdle_nid_postfix(); + if (region.Word(LabelOffset) != LabelValue) failure = "the label after the fill had not landed by the device wait"; + else if (const auto mismatches = Mismatches(region, false); !mismatches.empty()) failure = "after the label and the device wait: " + mismatches; + } + AgcDriver::Graphics::StorageTexture::FlushPending(target, region.bytes, nullptr, "test release"); + std::printf("linear target direct write completion, %s: %s\n", name, failure.empty() ? "the later writer's values" : failure.c_str()); + if (!failure.empty()) failures.push_back(std::string(name) + ": " + failure); + return true; +} + +bool RunCpuStore(bool watched, const Region& region, std::vector& failures) { + const std::string name = std::string("fill, draw in block 0, CPU store in block 1, ") + (watched ? "write-watched" : "unwatched") + " memory"; + const auto target = region.Address(); + Import(region); + if (watched && !AgcDriver::GuestMemory::Watched(target, TargetBytes)) return false; + if (!watched) { + AgcDriver::GuestMemory::Unwatch(target, region.bytes); + Require(!AgcDriver::GuestMemory::Watched(target, region.bytes), "linear target direct write completion: the region is still write-watched"); + } + std::vector copied(TargetBytes / 4u + 4u, Untouched); + GuestAllocations::Mutation().Add(copied.data(), TargetBytes + 16u, true, true, true); + struct Unregister { + void* data; + ~Unregister() { GuestAllocations::Mutation().Remove(data); } + } unregister{copied.data()}; + Gate.store(0, std::memory_order_release); + std::vector commands; + AppendFill(commands, target, copied.data(), BlockBytes); + AppendDraw(commands, target, BlockZeroScissor); + AppendGate(commands, WaitAtLeast, 1); + AppendLabel(commands, target + LabelOffset); + AppendGate(commands, WaitAtLeast, 2); + Submit(commands); + const auto completed = [&] { return std::atomic_ref(copied[0]).load(std::memory_order_acquire) == Filled; }; + bool pendingAtStore = false; + const bool recorded = Await([&] { + std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex()); + if (AgcDriver::Graphics::StorageTexture::FindLive(target, TargetBytes) == nullptr) return false; + pendingAtStore = !completed(); + region.Put(CpuStoreOffset, CpuStored); + return true; + }); + Gate.store(1, std::memory_order_release); + std::string failure; + bool completedAtLabel = false; + if (!recorded) failure = "the draw into block 0 never made the target's image"; + else if (!Await([&] { return region.Word(LabelOffset) == LabelValue; })) failure = "the label after the draw never landed"; + else { + completedAtLabel = completed(); + failure = CpuStoreMismatches(region); + if (!failure.empty()) failure = "when the label after the draw landed: " + failure; + if (watched && (!pendingAtStore || !completedAtLabel)) failure += std::string(failure.empty() ? "" : "; ") + (pendingAtStore ? "the fill's completion had not run when the label landed" : "the fill's completion had run before the CPU store") + ", so the ordering under test did not occur"; + } + const bool watchedAtLabel = AgcDriver::GuestMemory::Watched(target, TargetBytes); + Gate.store(2, std::memory_order_release); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::Graphics::StorageTexture::FlushPending(target, region.bytes, nullptr, "test release"); + std::printf("linear target direct write completion, %s (watched at the label: %d, completion pending at the CPU store: %d, run by the label: %d): %s\n", name.c_str(), watchedAtLabel ? 1 : 0, pendingAtStore ? 1 : 0, completedAtLabel ? 1 : 0, failure.empty() ? "the draw's color and the CPU store" : failure.c_str()); + if (!failure.empty()) failures.push_back(name + ": " + failure); + return true; +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).hostImportAlignment == 0) { + std::puts("linear target direct write completion tests skipped, host imports are unavailable on this device, so the target is never resident"); + return VulkanTestSkipped; + } + } + std::vector> regions; + struct ShutdownGuard { + ~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} } + } shutdown; + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + const auto unwatchedImports = [] { + std::puts("linear target direct write completion tests skipped, the device does not keep host imports write-watched, so no completion stamp over the target is tracked"); + return VulkanTestSkipped; + }; + std::vector failures; + for (const auto order : {Order::FillFirst, Order::DrawsFirst}) { + regions.push_back(std::make_unique(RegionBytes)); + if (!Run(order, *regions.back(), failures)) return unwatchedImports(); + } + for (const bool watched : {true, false}) { + regions.push_back(std::make_unique(RegionBytes)); + if (!RunCpuStore(watched, *regions.back(), failures)) return unwatchedImports(); + } + if (!failures.empty()) { + std::string message = "linear target direct write completion: at a completion point, the memory of a resident linear target did not hold the later of a direct buffer write with completion work and draws into it, or lost a CPU store made outside the draws:"; + for (const auto& failure : failures) message += "\n " + failure; + Require(false, message); + } + AgcDriverShutdown_nid_postfix(); + std::puts("linear target direct write completion 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/LinearTargetDispatchAccess.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDispatchAccess.cpp new file mode 100644 index 0000000000..809ffd4d91 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDispatchAccess.cpp @@ -0,0 +1,300 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.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 + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t Threads = 64; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t TargetBytes = std::size_t{Width} * Height * 16u; +constexpr std::uint32_t Rgba32FloatInfo = 0x8738u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32AbgrExport = 9; +constexpr std::uint32_t ReadX = 10; +constexpr std::uint32_t ReadY = 6; +constexpr std::uint32_t Drawn = 0x3f800000u; +constexpr std::uint32_t Unwritten = 0xdeadbeefu; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array CopyCode{0x7e000280u, 0xdc308000u, 0x01000000u, 0xbf8c3f70u, 0xdc708000u, 0x00020100u, 0xbf810000u}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 2> Programs{}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::span Program(std::size_t index, bool blue) { + auto& code = Programs.at(index); + std::size_t at = 0; + code[at++] = 0x36000000u | (20u << 17u) | 0xffu; + code[at++] = 0x0000ffffu; + code[at++] = 0x2c000000u | (21u << 17u) | 0x90u; + code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u); + code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u); + code[at++] = (blue ? 0x7e0002f2u : 0x7e000280u) | (22u << 17u); + code[at++] = 0x7e0002f2u | (23u << 17u); + code[at++] = 0xf800180fu; + code[at++] = 0x17161514u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map a guest block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate a guest block"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "a guest block is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Rgba32FloatInfo; + cx[0x31d] = 0; + cx[0x323] = 0; + cx[0x324] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = LinearAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + color.exportIndex = 0; + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::span pixelCode, VkRect2D scissor) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.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 vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionFixedPoint); + pixel.targetOutputMode.at(0) = Float32AbgrExport; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + VkPipelineColorBlendAttachmentState blend{}; + blend.blendEnable = VK_FALSE; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = scissor; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::uint64_t source, std::uint64_t destination) { + constexpr std::uint32_t waveSize = 64; + const std::array userData{static_cast(source), static_cast(source >> 32u), static_cast(destination), static_cast(destination >> 32u)}; + const std::span code(CopyCode); + 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, {}}, + {waveSize, 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())); +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t Dword(const std::uint8_t* bytes) { + std::uint32_t value = 0; + std::memcpy(&value, bytes, sizeof(value)); + return value; +} + +bool Expect(AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if (!context.bufferDeviceAddress) { + std::puts("skipped, the device lacks bufferDeviceAddress"); + return false; + } + Block block(16 * BlockBytes); + std::fill(block.data, block.data + block.bytes, std::uint8_t{0}); + Block output(BlockBytes); + for (std::size_t offset = 0; offset < output.bytes; offset += sizeof(Unwritten)) std::memcpy(output.data + offset, &Unwritten, sizeof(Unwritten)); + if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) { + std::puts("skipped, host imports are unavailable"); + return false; + } + const auto color = DecodeTarget(block); + Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && color.bytes == TargetBytes, "the target is not a linear R32G32B32A32 target"); + const auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr, "the device has no recorder"); + Draw(device, color, Program(0, false), {{0, 0}, {Width, Height}}); + device.WaitIdle(); + Require(StorageTexture::FindPending(block.Address(), TargetBytes) == nullptr, "the linear target was still pending after the device wait"); + const auto blueOffset = (static_cast(ReadY) * Width + ReadX) * 16u + 8u; + Require(Dword(block.data + blueOffset) == 0u, "the first draw left " + Hex(Dword(block.data + blueOffset)) + " in the blue channel of pixel (" + std::to_string(ReadX) + ", " + std::to_string(ReadY) + ")"); + Draw(device, color, Program(1, true), {{8, 4}, {16, 8}}); + Require(recorder->Recording(), "the draw into the linear target was not left in the open batch"); + Dispatch(device, block.Address() + blueOffset, output.Address()); + device.WaitIdle(); + const auto read = Dword(output.data); + Require(read == Drawn, "the address-based dispatch recorded after the draw read " + Hex(read) + " from the blue channel of pixel (" + std::to_string(ReadX) + ", " + std::to_string(ReadY) + "), not the drawn " + Hex(Drawn)); + Require(Dword(block.data + blueOffset) == Drawn, "the second draw left " + Hex(Dword(block.data + blueOffset)) + " in the blue channel of pixel (" + std::to_string(ReadX) + ", " + std::to_string(ReadY) + ")"); + return true; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!Expect(*device)) return VulkanTestSkipped; + std::puts("linear target dispatch access 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/LinearTargetDrawBindings.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDrawBindings.cpp new file mode 100644 index 0000000000..4335e86e2c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDrawBindings.cpp @@ -0,0 +1,334 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.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" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.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 + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t TargetBytes = std::size_t{Width} * Height * 16u; +constexpr std::size_t CopyTargetOffset = 0x40000; +constexpr std::size_t FirstSourceOffset = 0x80000; +constexpr std::size_t SecondSourceOffset = 0x90000; +constexpr std::size_t PointedOffset = 0xa0000; +constexpr std::uint32_t Rgba32FloatInfo = 0x8738u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32AbgrExport = 9; +constexpr std::uint32_t FirstSource = 0x3e800000u; +constexpr std::uint32_t SecondSource = 0x3f400000u; +constexpr std::uint32_t Pointed = 0x3f800000u; +constexpr std::uint32_t Unwritten = 0xdeadbeefu; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelOpaque{0x7e100280u, 0x7e120280u, 0x7e140280u, 0x7e1602f2u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; +alignas(256) constexpr std::array PixelRead{0x7e1a0280u, 0xdc308000u, 0x0c02000du, 0xbf8c3f70u, 0x7e10030cu, 0x7e120280u, 0x7e140280u, 0x7e160280u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; +alignas(256) constexpr std::array PixelCopy{0x7e040280u, 0xe0301000u, 0x80010102u, 0xbf8c3f70u, 0xe0701000u, 0x80020102u, 0x7e100301u, 0x7e120280u, 0x7e140280u, 0x7e1602f2u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array FirstOutput{}; +alignas(256) std::array SecondOutput{}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::array RawBuffer(std::uint64_t address, std::uint32_t bytes) { + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color targets"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color targets"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color targets are not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(address >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Rgba32FloatInfo; + cx[0x31d] = 0; + cx[0x323] = 0; + cx[0x324] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = LinearAttrib3; + cx[0x390] = static_cast(address >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + color.exportIndex = 0; + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::span pixelCode, std::span pixelUserData, VkRect2D scissor) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.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 vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionFixedPoint); + pixel.targetOutputMode.at(0) = Float32AbgrExport; + pixel.targetExportMapping.fill(0xe4u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + VkPipelineColorBlendAttachmentState blend{}; + blend.blendEnable = VK_FALSE; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = scissor; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::array CopyUserData(std::uint64_t source, const std::array& output) { + std::array userData{}; + const auto read = RawBuffer(source, 16u); + const auto written = RawBuffer(reinterpret_cast(output.data()), 16u); + std::copy(read.begin(), read.end(), userData.begin() + 4); + std::copy(written.begin(), written.end(), userData.begin() + 8); + return userData; +} + +std::array Texel(const std::uint8_t* target, std::uint32_t x, std::uint32_t y) { + std::array texel{}; + std::memcpy(texel.data(), target + (static_cast(y) * Width + x) * 16u, sizeof(texel)); + return texel; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::string Describe(const std::array& texel) { + return "(" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + ")"; +} + +void ExpectTexels(const std::uint8_t* target, const char* what, std::uint32_t firstX, std::uint32_t lastX, const std::array& expected) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = firstX; x < lastX; ++x) { + const auto texel = Texel(target, x, y); + Require(texel == expected, std::string(what) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds " + Describe(texel) + ", expected " + Describe(expected)); + } + } +} + +bool Expect(AgcDriver::VulkanDevice& device, bool unwatch) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if (!context.bufferDeviceAddress) { + std::puts("skipped, the device lacks bufferDeviceAddress"); + return false; + } + Block block(16 * BlockBytes); + std::fill(block.data, block.data + block.bytes, std::uint8_t{0}); + const auto put = [&](std::size_t offset, std::uint32_t value, std::size_t dwords) { + for (std::size_t dword = 0; dword < dwords; ++dword) std::memcpy(block.data + offset + dword * 4u, &value, sizeof(value)); + }; + put(FirstSourceOffset, FirstSource, 4); + put(SecondSourceOffset, SecondSource, 4); + put(PointedOffset, Pointed, 1); + FirstOutput.fill(Unwritten); + SecondOutput.fill(Unwritten); + if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) { + std::puts("skipped, host imports are unavailable"); + return false; + } + const auto drawn = DecodeTarget(block.Address()); + const auto copied = DecodeTarget(block.Address() + CopyTargetOffset); + Require(drawn.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && drawn.bytes == TargetBytes && copied.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && copied.bytes == TargetBytes, "the targets are not linear R32G32B32A32 targets"); + auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr && context.descriptorCache != nullptr, "the device has no recorder or descriptor cache"); + const auto pending = [](std::uint64_t address) { return StorageTexture::FindPending(address, TargetBytes) != nullptr; }; + const VkRect2D whole{{0, 0}, {Width, Height}}; + Draw(device, drawn, PixelOpaque, {}, whole); + Draw(device, copied, PixelOpaque, {}, whole); + device.WaitIdle(); + Require(!pending(drawn.address) && !pending(copied.address), "a linear target was still pending after the device wait"); + if (unwatch) AgcDriver::GuestMemory::Unwatch(block.Address(), block.bytes); + const bool watched = AgcDriver::GuestMemory::Watched(copied.address, TargetBytes); + Require(!unwatch || !watched, "the copied target is still write-watched after its block was taken out of the write watch"); + const auto submitted = recorder->Submissions(); + const auto pointed = block.Address() + PointedOffset; + const std::array pointer{0u, 0u, static_cast(pointed), static_cast(pointed >> 32u)}; + Draw(device, drawn, PixelRead, pointer, whole); + Require(pending(drawn.address), "the address-based draw did not leave its linear target pending"); + Require(recorder->Recording() && recorder->Submissions() == submitted, "the address-based draw did not stay in the open batch"); + const auto setsBefore = context.descriptorCache->Counters().sets; + const auto firstCopy = CopyUserData(block.Address() + FirstSourceOffset, FirstOutput); + Draw(device, copied, PixelCopy, firstCopy, {{0, 0}, {Width / 2u, Height}}); + Require(!pending(drawn.address), "the draw writing a copied buffer did not store the drawn linear target first"); + Require(pending(copied.address), "the first copying draw did not leave its linear target pending"); + Require(recorder->Recording() && recorder->Submissions() == submitted + 1, "storing the drawn linear target did not submit the batch the address-based draw read it in (" + std::to_string(recorder->Submissions() - submitted) + " submissions)"); + const auto setsBetween = context.descriptorCache->Counters().sets; + const auto secondCopy = CopyUserData(block.Address() + SecondSourceOffset, SecondOutput); + Draw(device, copied, PixelCopy, secondCopy, {{static_cast(Width / 2u), 0}, {Width / 2u, Height}}); + if (watched) Require(recorder->Recording() && recorder->Submissions() == submitted + 1, "the second copying draw submitted recorded work"); + else Require(recorder->Submissions() == submitted + 2, "the second copying draw into a stored target outside the write watch did not submit once to capture its bytes (" + std::to_string(recorder->Submissions() - submitted - 1) + " submissions)"); + const auto setsAfter = context.descriptorCache->Counters().sets; + device.WaitIdle(); + Require(FirstOutput[0] == FirstSource, "the first copying draw's output holds " + Hex(FirstOutput[0]) + ", not its source's " + Hex(FirstSource)); + Require(SecondOutput[0] == SecondSource, "the second copying draw's output holds " + Hex(SecondOutput[0]) + ", not its source's " + Hex(SecondSource)); + ExpectTexels(block.data + CopyTargetOffset, "the first copying draw's half", 0, Width / 2u, {0.25f, 0.0f, 0.0f, 1.0f}); + ExpectTexels(block.data + CopyTargetOffset, "the second copying draw's half", Width / 2u, Width, {0.75f, 0.0f, 0.0f, 1.0f}); + ExpectTexels(block.data, "the address-based draw's target", 0, Width, {1.0f, 0.0f, 0.0f, 0.0f}); + Require(setsAfter - setsBetween == setsBetween - setsBefore, "the second copying draw allocated " + std::to_string(setsAfter - setsBetween) + " descriptor sets, the first " + std::to_string(setsBetween - setsBefore)); + return true; +} + +} + +int main() { +#ifdef _WIN32 + _putenv_s("APS5_RELEASE_ON_UNLOCK", "1"); +#else + setenv("APS5_RELEASE_ON_UNLOCK", "1", 1); +#endif + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!Expect(*device, false)) return VulkanTestSkipped; + if (!Expect(*device, true)) return VulkanTestSkipped; + std::puts("linear target draw binding 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/LinearTargetRectangleBound.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetRectangleBound.cpp new file mode 100644 index 0000000000..c19854ea05 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetRectangleBound.cpp @@ -0,0 +1,344 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.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 + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t RegionBytes = 8 * BlockBytes; +constexpr std::uint32_t Rgba32FloatInfo = 0x8738u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32AbgrExport = 9; +constexpr std::uint8_t Initial = 0x3f; +constexpr std::uint8_t Sentinel = 0x37; +constexpr std::uint8_t Gap = 0x29; +constexpr std::size_t GapBytes = 0x100; + +struct Surface { + std::size_t offset; + std::uint32_t width; + std::uint32_t height; + std::size_t RowBytes() const { return std::size_t{width} * 16u; } + std::size_t Bytes() const { return RowBytes() * height; } +}; + +constexpr Surface Tall{0, 64, 256}; +constexpr Surface Shifted{6 * BlockBytes + GapBytes, 64, 16}; +constexpr VkRect2D Seed{{63, 255}, {1, 1}}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 2> Programs{}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::span Program(std::size_t index, bool blue) { + auto& code = Programs.at(index); + std::size_t at = 0; + code[at++] = 0x36000000u | (20u << 17u) | 0xffu; + code[at++] = 0x0000ffffu; + code[at++] = 0x2c000000u | (21u << 17u) | 0x90u; + code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u); + code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u); + code[at++] = (blue ? 0x7e0002f2u : 0x7e000280u) | (22u << 17u); + code[at++] = 0x7e0002f2u | (23u << 17u); + code[at++] = 0xf800180fu; + code[at++] = 0x17161514u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), arena(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (arena) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (arena) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color targets"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color targets"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (arena) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (arena) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool arena; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, const Surface& surface) { + const auto address = block.Address() + surface.offset; + AgcDriver::Registers cx; + cx[0x318] = static_cast(address >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Rgba32FloatInfo; + cx[0x31d] = 0; + cx[0x323] = 0; + cx[0x324] = 0; + cx[0x3b0] = ((surface.width - 1u) << 14u) | (surface.height - 1u); + cx[0x3b8] = LinearAttrib3; + cx[0x390] = static_cast(address >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + color.exportIndex = 0; + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, const Surface& surface, std::span pixelCode, VkRect2D scissor) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.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 vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionFixedPoint); + pixel.targetOutputMode.at(0) = Float32AbgrExport; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + VkPipelineColorBlendAttachmentState blend{}; + blend.blendEnable = VK_FALSE; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {surface.width, surface.height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(surface.height), static_cast(surface.width), -static_cast(surface.height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = scissor; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +bool Inside(const VkRect2D& rect, std::uint32_t x, std::uint32_t y) { + return x >= static_cast(rect.offset.x) && x < static_cast(rect.offset.x) + rect.extent.width && y >= static_cast(rect.offset.y) && y < static_cast(rect.offset.y) + rect.extent.height; +} + +std::uint8_t* Pixel(const Block& block, const Surface& surface, std::uint32_t x, std::uint32_t y) { + return block.data + surface.offset + static_cast(y) * surface.RowBytes() + static_cast(x) * 16u; +} + +bool Holds(const std::uint8_t* pixel, std::uint8_t value) { + return std::all_of(pixel, pixel + 16, [&](std::uint8_t byte) { return byte == value; }); +} + +bool HoldsOutput(const std::uint8_t* pixel, std::uint32_t x, std::uint32_t y, float z) { + std::array texel{}; + std::memcpy(texel.data(), pixel, sizeof(texel)); + return texel == std::array{static_cast(x), static_cast(y), z, 1.0f}; +} + +bool Expect(AgcDriver::VulkanDevice& device) { + Block block(RegionBytes); + std::fill(block.data, block.data + block.bytes, Initial); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) return false; + const auto tall = DecodeTarget(block, Tall); + const auto shifted = DecodeTarget(block, Shifted); + Require(tall.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && tall.bytes == Tall.Bytes() && shifted.bytes == Shifted.Bytes(), "the targets are not linear R32G32B32A32 targets"); + constexpr VkRect2D first{{8, 4}, {16, 16}}; + constexpr VkRect2D second{{16, 12}, {24, 16}}; + constexpr VkRect2D whole{{0, 0}, {Shifted.width, Shifted.height}}; + Draw(device, tall, Tall, Program(0, false), Seed); + device.WaitIdle(); + Draw(device, tall, Tall, Program(0, false), first); + Draw(device, tall, Tall, Program(1, true), second); + Draw(device, shifted, Shifted, Program(0, false), whole); + const std::array, 3> sentinels{{{50, 10}, {20, 40}, {30, 200}}}; + const std::array, 2> between{{{30, 6}, {10, 25}}}; + for (const auto& [x, y] : sentinels) std::memset(Pixel(block, Tall, x, y), Sentinel, 16); + for (const auto& [x, y] : between) std::memset(Pixel(block, Tall, x, y), Sentinel, 16); + Require((block.Address() + Shifted.offset) / BlockBytes * BlockBytes == block.Address() + Shifted.offset - GapBytes, "the shifted target does not start past a 64 KiB boundary"); + std::memset(block.data + Shifted.offset - GapBytes, Gap, GapBytes); + device.WaitIdle(); + std::size_t wrongDrawn = 0, wrongSentinels = 0, wrongBetween = 0, wrongUntouched = 0, wrongShifted = 0; + for (std::uint32_t y = 0; y < Tall.height; ++y) { + for (std::uint32_t x = 0; x < Tall.width; ++x) { + const auto* pixel = Pixel(block, Tall, x, y); + const auto at = [&](const auto& point) { return point[0] == x && point[1] == y; }; + if (std::any_of(sentinels.begin(), sentinels.end(), at)) wrongSentinels += Holds(pixel, Sentinel) ? 0u : 1u; + else if (std::any_of(between.begin(), between.end(), at)) wrongBetween += Holds(pixel, Sentinel) ? 0u : 1u; + else if (Inside(second, x, y)) wrongDrawn += HoldsOutput(pixel, x, y, 1.0f) ? 0u : 1u; + else if (Inside(first, x, y) || Inside(Seed, x, y)) wrongDrawn += HoldsOutput(pixel, x, y, 0.0f) ? 0u : 1u; + else wrongUntouched += Holds(pixel, Initial) ? 0u : 1u; + } + } + for (std::uint32_t y = 0; y < Shifted.height; ++y) { + for (std::uint32_t x = 0; x < Shifted.width; ++x) wrongShifted += HoldsOutput(Pixel(block, Shifted, x, y), x, y, 0.0f) ? 0u : 1u; + } + const auto gapLost = static_cast(std::count_if(block.data + Shifted.offset - GapBytes, block.data + Shifted.offset, [](std::uint8_t byte) { return byte != Gap; })); + std::printf("linear target rectangle bound: wrong drawn pixels %zu, lost sentinels %zu/%zu, lost sentinels between the rectangles %zu/%zu, changed untouched pixels %zu, wrong shifted-target pixels %zu, lost gap bytes %zu/%zu\n", wrongDrawn, wrongSentinels, sentinels.size(), wrongBetween, between.size(), wrongUntouched, wrongShifted, gapLost, GapBytes); + Require(wrongDrawn == 0 && wrongShifted == 0, "a drawn linear target's store lost pixels its draws wrote"); + Require(wrongSentinels == 0 && gapLost == 0, "a drawn linear target's store overwrote CPU stores outside the drawn rectangles"); + Require(wrongBetween == 0, "a drawn linear target's store overwrote CPU stores between two draws' scissor rectangles"); + Require(wrongUntouched == 0, "a drawn linear target's store changed pixels no draw and no CPU store wrote"); + return true; +} + +bool ExpectManyRows(AgcDriver::VulkanDevice& device) { + Block block(RegionBytes); + std::fill(block.data, block.data + block.bytes, Initial); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) return false; + const auto tall = DecodeTarget(block, Tall); + constexpr std::uint32_t Rows = 17; + constexpr std::uint32_t Spacing = 15; + Draw(device, tall, Tall, Program(0, false), Seed); + device.WaitIdle(); + for (std::uint32_t row = 0; row < Rows; ++row) Draw(device, tall, Tall, Program(0, false), VkRect2D{{8, static_cast(row * Spacing)}, {16, 1}}); + constexpr std::array between{10, 70}; + std::memset(Pixel(block, Tall, between[0], between[1]), Sentinel, 16); + device.WaitIdle(); + std::size_t wrongDrawn = 0, wrongUntouched = 0; + for (std::uint32_t y = 0; y < Tall.height; ++y) { + const bool drawnRow = y % Spacing == 0 && y / Spacing < Rows; + for (std::uint32_t x = 0; x < Tall.width; ++x) { + if (x == between[0] && y == between[1]) continue; + const auto* pixel = Pixel(block, Tall, x, y); + if ((drawnRow && x >= 8 && x < 24) || Inside(Seed, x, y)) wrongDrawn += HoldsOutput(pixel, x, y, 0.0f) ? 0u : 1u; + else wrongUntouched += Holds(pixel, Initial) ? 0u : 1u; + } + } + const bool kept = Holds(Pixel(block, Tall, between[0], between[1]), Sentinel); + std::printf("linear target rectangle bound, %u one-row scissors: wrong drawn pixels %zu, sentinel between the rows %s, changed untouched pixels %zu\n", Rows, wrongDrawn, kept ? "kept" : "lost", wrongUntouched); + Require(wrongDrawn == 0, "a drawn linear target's store lost pixels its one-row draws wrote"); + Require(kept, "a drawn linear target's store overwrote a CPU store between two one-row scissors"); + Require(wrongUntouched == 0, "a drawn linear target's store changed pixels no one-row draw and no CPU store wrote"); + return true; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!Expect(*device) || !ExpectManyRows(*device)) { + std::puts("linear target rectangle bound test skipped: no host import on this device"); + return VulkanTestSkipped; + } + std::puts("linear target rectangle bound 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/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index 5548d5d7fa..3d3531a577 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -671,6 +671,81 @@ void CheckDrawFeedback() { std::puts("Native depth feedback refused through the draw resource path; a written surface refused and a depth-tested one retired on guest read"); } +void CheckDepthAttachmentAlias() { + const auto device = OpenVulkanTestDevice(); + Require(device != nullptr, "native depth attachment alias requires Vulkan"); + const auto context = device->graphicsContext(); + constexpr VkExtent2D extent{5u, 3u}; + constexpr std::uint32_t samples = 4u; + const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, 4u, samples)); + TargetMemory memory(bytes), firstMetadata(65536u), secondMetadata(65536u); + std::memset(memory.Data(), 0x3c, bytes); + std::memset(firstMetadata.Data(), 0, 65536u); + std::memset(secondMetadata.Data(), 0, 65536u); + struct Cleanup { + AgcDriver::VulkanDevice& device; + ~Cleanup() { device.WaitIdle(); ClearCachedTextures(device.Device()); ClearDepthSurfaces(device.Device()); } + } cleanup{*device}; + DepthTarget first{memory.Address(), 0u, extent, VK_FORMAT_D32_SFLOAT, 0.0f, 0u, firstMetadata.Address(), false, samples}; + first.depthSwizzle = 24u; + first.iterate256 = first.htilePipeAligned = first.htileDepthOnly = true; + const auto firstView = DepthSurfaceView(context, first); + const auto words = DepthSampleWords(first); + auto kept = DepthSurfaceTexture(context, words, DecodeTextureResource(words), VkComponentMapping{}); + Require(kept != nullptr, "attachment alias fixture has no held native depth view"); + auto second = first; + second.extent.width = 6u; + second.htileAddress = secondMetadata.Address(); + Require(MultisampledDepthBytes_nid_no_patch(second.extent, 4u, samples) <= bytes, "attachment alias fixture exceeds its backing"); + const auto secondView = DepthSurfaceView(context, second); + Require(secondView != VK_NULL_HANDLE && secondView != firstView, "an aliased depth attachment reused the previous layout's view"); + Require(DepthSurfaceView(context, second) == secondView, "an unchanged native depth attachment was not reused"); + for (std::size_t offset = 0u; offset < bytes; ++offset) Require(memory.Data()[offset] == std::byte{0x3c}, "retiring an unwritten attachment alias changed guest bytes"); + std::puts("native depth attachment alias changes layout while retaining held GPU views"); +} + +void CheckStorageAliasRetirement() { + const auto device = OpenVulkanTestDevice(); + Require(device != nullptr, "native depth storage alias requires Vulkan"); + const auto context = device->graphicsContext(); + constexpr VkExtent2D extent{5u, 3u}; + constexpr std::uint32_t samples = 4u; + constexpr std::size_t prefix = 65536u; + const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, 4u, samples)); + TargetMemory memory(prefix + bytes), metadata(65536u); + std::memset(memory.Data(), 0x3c, prefix + bytes); + std::memset(metadata.Data(), 0, 65536u); + struct Cleanup { + AgcDriver::VulkanDevice& device; + ~Cleanup() { device.WaitIdle(); ClearCachedTextures(device.Device()); ClearDepthSurfaces(device.Device()); } + } cleanup{*device}; + GuestTextureResource alias{}; + alias.baseAddress = memory.Address(); + alias.width = 256u; + alias.height = static_cast((prefix + bytes) / 1024u); + alias.mipCount = alias.allocatedMipCount = 1u; + alias.dimension = TextureDimension::k2D; + alias.tileMode = TextureTileMode::kLinear; + alias.format = 22u; + alias.dstSelX = 0u; + alias.dstSelY = alias.dstSelZ = 4u; + alias.dstSelW = 5u; + auto cached = CachedStorageSurface(context, alias); + Require(cached != nullptr, "storage alias was not cached before native depth creation"); + DepthTarget depth{memory.Address() + prefix, 0u, extent, VK_FORMAT_D32_SFLOAT, 0.0f, 0u, metadata.Address(), false, samples}; + depth.depthSwizzle = 24u; + depth.iterate256 = depth.htilePipeAligned = depth.htileDepthOnly = true; + Require(DepthSurfaceView(context, depth) != VK_NULL_HANDLE && DepthSurfaceAt(depth.address), "storage alias fixture has no native depth owner"); + auto disjoint = alias; + disjoint.height = static_cast(prefix / 1024u); + Require(CachedStorageSurface(context, disjoint) != nullptr, "disjoint storage alias was not created"); + Require(DepthSurfaceAt(depth.address), "storage access ending at native depth retired a disjoint owner"); + Require(CachedStorageSurface(context, alias) != nullptr, "overlapping cached storage alias was not returned"); + Require(!DepthSurfaceAt(depth.address), "a cached storage view kept a native depth owner inside its range"); + for (std::size_t offset = 0u; offset < prefix + bytes; ++offset) Require(memory.Data()[offset] == std::byte{0x3c}, "retiring unwritten native depth changed guest bytes"); + std::puts("cached storage span retires only intersecting native depth owners"); +} + enum class ClearFault { None, PartialStore, CpuWrite, DepthWrite, BadMetadata, UnprovenDispatch, PendingWrite, Overlap, PossibleStore, CompletedPossibleStore, DriverDispatch, ImportWindowWrite }; void CheckClearDispatch() { @@ -926,10 +1001,10 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, Require(probe[0] == std::byte{0xa5}, "resident depth guard blocked an unrelated read"); auto alias = target; alias.htileAddress += 65536u; - TargetRefuses([&] { DepthSurfaceView(context, alias); }, "layout or metadata changed"); + TargetRefuses([&] { DepthSurfaceView(context, alias); }, "sample-preserving guest publication"); alias = target; alias.htileDepthOnly = !target.htileDepthOnly; - TargetRefuses([&] { DepthSurfaceView(context, alias); }, "layout or metadata changed"); + TargetRefuses([&] { DepthSurfaceView(context, alias); }, "sample-preserving guest publication"); auto resource = DecodeTextureResource(words); for (std::uint32_t field = 0u; field < 18u; ++field) { auto changed = resource; @@ -1041,7 +1116,7 @@ void CheckHtileRouting(Context context, VkFormat format, std::uint32_t fragments auto alias = target; alias.samples = 1u; alias.address = single.Address(); - TargetRefuses([&] { DepthSurfaceView(context, alias); }, "layout or metadata changed"); + Require(DepthSurfaceView(context, alias) != VK_NULL_HANDLE, "a single-sample bind could not retire an unwritten native HTILE owner"); Require(untouched(), "a single-sample bind over a native owner's HTILE stored expanded words into it"); std::printf("Native depth format=%u samples=%u: binds over the owner's HTILE left its words alone\n", static_cast(format), fragments); } @@ -1303,6 +1378,8 @@ int main(int argc, char** argv) { } CheckDrawFeedback(); const auto context = device->GetContext(); + if (argc == 1) CheckDepthAttachmentAlias(); + if (argc == 1) CheckStorageAliasRetirement(); std::unique_lock lock(AgcDriver::GuestMemory::GpuMutex()); Require(DepthSupported(context, VK_FORMAT_D32_SFLOAT, 4u), "the native depth regression requires sampled D32 with four samples"); if (argc == 2 && std::string_view(argv[1]) == "--clear-refusals") { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp index f2b56411c0..2ff511dcd1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp @@ -64,6 +64,8 @@ struct PipelineTrace { std::mutex mutex; std::condition_variable released; bool allowOptimization = false; + std::uint32_t libraries = 0; + std::uint32_t retaining = 0; std::vector fast; std::vector optimized; VkCommandBuffer boundCommands = VK_NULL_HANDLE; @@ -90,7 +92,12 @@ VKAPI_ATTR VkResult VKAPI_CALL CreatePipelines(VkDevice device, VkPipelineCache const auto result = Trace->create(device, cache, count, infos, allocator, pipelines); std::lock_guard lock(Trace->mutex); for (std::uint32_t index = 0; index < count; ++index) { - if (pipelines[index] == VK_NULL_HANDLE || (infos[index].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) != 0) continue; + if (pipelines[index] == VK_NULL_HANDLE) continue; + if ((infos[index].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) != 0) { + ++Trace->libraries; + if ((infos[index].flags & VK_PIPELINE_CREATE_RETAIN_LINK_TIME_OPTIMIZATION_INFO_BIT_EXT) != 0) ++Trace->retaining; + continue; + } auto& handles = (infos[index].flags & VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT) != 0 ? Trace->optimized : Trace->fast; handles.push_back(pipelines[index]); } @@ -217,12 +224,17 @@ void Run(AgcDriver::VulkanDevice& device, Context context, PipelineTrace& trace, CheckPixels(masked, phase); }; render(false, "fast link"); + { + std::lock_guard lock(trace.mutex); + Require(trace.libraries != 0, "the readback pipeline's libraries were not created through the traced device"); + Require(trace.retaining == (optimize ? trace.libraries : 0u), std::to_string(trace.retaining) + " of the " + std::to_string(trace.libraries) + " libraries were created to retain link-time optimization info, expected " + (optimize ? "all of them" : "none, since no optimized link will be made")); + } trace.ReleaseOptimization(); WaitForOptimizedPipelines(context.device); Require(PipelineLibraryCountersOf(context.device).linked == 1, "the readback pipeline was not linked from graphics libraries"); Require(trace.optimized.size() == (optimize ? 1u : 0u), "the optimized pipeline creation count differs from the requested mode"); render(optimize, "completed link"); - std::printf("pipeline readback passed: fast draws %u, optimized draws %u, optimized creations %zu\n", optimize ? 1u : 2u, optimize ? 1u : 0u, trace.optimized.size()); + std::printf("pipeline readback passed: fast draws %u, optimized draws %u, optimized creations %zu, libraries %u (%u retaining link-time optimization info)\n", optimize ? 1u : 2u, optimize ? 1u : 0u, trace.optimized.size(), trace.libraries, trace.retaining); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp index 8cc143e3c5..4996854346 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp @@ -132,10 +132,8 @@ void Submit(std::vector& words) { int main() { #ifdef _WIN32 _putenv_s("APS5_PIPELINED_DRAWS", "1"); - _putenv_s("APS5_PIPELINE_DMA", "1"); #else setenv("APS5_PIPELINED_DRAWS", "1", 1); - setenv("APS5_PIPELINE_DMA", "1", 1); #endif try { { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp index d43e135c35..553e26c52f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -43,13 +44,13 @@ void Submit(std::vector& words) { } struct Gate { - std::atomic release{false}; - ~Gate() { release.store(true, std::memory_order_release); } + std::shared_ptr> release = std::make_shared>(false); + ~Gate() { release->store(true, std::memory_order_release); } }; void Hold(Gate& gate, std::uint64_t address, std::size_t bytes) { - DrawPipeline::Queue0().Enqueue([&gate] { - while (!gate.release.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + DrawPipeline::Queue0().Enqueue([release = gate.release] { + while (!release->load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); }, {{address, address + bytes}}); } @@ -58,7 +59,7 @@ void ExpectDrains(std::vector& words, std::uint32_t opcode, Gate& std::thread releaser([&] { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); while (DrawPipeline::DrainRequestsByOpcode(opcode) == before && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); - gate.release.store(true, std::memory_order_release); + gate.release->store(true, std::memory_order_release); }); Submit(words); releaser.join(); @@ -74,10 +75,8 @@ void ExpectNoDrain(std::uint32_t opcode, std::uint64_t before, const char* what) int main() { #ifdef _WIN32 _putenv_s("APS5_PIPELINED_DRAWS", "1"); - _putenv_s("APS5_PIPELINE_DMA", "1"); #else setenv("APS5_PIPELINED_DRAWS", "1", 1); - setenv("APS5_PIPELINE_DMA", "1", 1); #endif try { alignas(256) static std::array first{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedMetadataPass.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedMetadataPass.cpp index 7fa9eb1f07..0410e4c9a6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedMetadataPass.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedMetadataPass.cpp @@ -120,11 +120,9 @@ void expectEliminated(const std::string& what) { int main() { #ifdef _WIN32 _putenv_s("APS5_PIPELINED_DRAWS", "1"); - _putenv_s("APS5_PIPELINE_DMA", "1"); _putenv_s("APS5_DRAW_PIPELINE", "1"); #else setenv("APS5_PIPELINED_DRAWS", "1", 1); - setenv("APS5_PIPELINE_DMA", "1", 1); setenv("APS5_DRAW_PIPELINE", "1", 1); #endif try { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedPrefetch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedPrefetch.cpp index cf3574966c..fe2b116f63 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedPrefetch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedPrefetch.cpp @@ -106,12 +106,12 @@ int main() { const auto prefetched = SubmitBehindBusyPipeline(prefetch, 0x11111111u, true); check(prefetched.outstanding >= 2, "a DMA_DATA prefetch drained the draw pipeline: the label behind it never queued behind the held commit (" + std::to_string(prefetched.outstanding) + " outstanding)"); check(prefetched.drains == 0, "a DMA_DATA prefetch drained the draw pipeline (" + std::to_string(prefetched.drains) + " packet drains)"); - const auto copied = SubmitBehindBusyPipeline(copy, 0x22222222u, false); - check(copied.outstanding == 1, "a DMA_DATA copy did not hold the queue worker (" + std::to_string(copied.outstanding) + " outstanding)"); - check(copied.drains == 1, "a DMA_DATA copy did not drain the draw pipeline (" + std::to_string(copied.drains) + " packet drains)"); + const auto copied = SubmitBehindBusyPipeline(copy, 0x22222222u, true); + check(copied.outstanding >= 2, "a DMA_DATA copy that misses every queued write drained the draw pipeline: the label behind it never queued behind the held commit (" + std::to_string(copied.outstanding) + " outstanding)"); + check(copied.drains == 0, "a DMA_DATA copy that misses every queued write drained the draw pipeline (" + std::to_string(copied.drains) + " packet drains)"); for (std::size_t i = 0; i < Destination.size(); ++i) check(Destination[i] == (i < 128 ? Source[i] : 0u), "the copy did not land, or wrote past its bytes"); LibcRunShutdown_nid_postfix(); - std::puts("a DMA_DATA prefetch keeps the draw pipeline, a copy drains it"); + std::puts("a DMA_DATA prefetch and a copy that misses every queued write keep the draw pipeline"); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 88ecd918f8..82c999000d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -1413,13 +1413,81 @@ void RelocatedHeaders() { ExpectFailure([&] { AgcDriverResolveShaderAbi_nid_postfix(&otherContext.shader, {}, primitive); }, "replaced shader header"); } +void DispatchAheadPrewarm() { + alignas(256) std::array pendingCode{0xbf810000u}; + alignas(256) std::array lazyCode{}; + lazyCode[0] = 0xbf810000u; + lazyCode[64] = 0xbf810000u; + struct Header { + Shader shader{}; + std::array registers{}; + ShaderSpecialRegs specials{}; + } pendingHeader, lazyHeader; + const auto fillHeader = [](Header& header, const void* code, std::size_t codeBytes) { + const auto address = reinterpret_cast(code); + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(codeBytes); + header.shader.code = const_cast(code); + header.shader.sh_registers = header.registers.data(); + header.shader.num_sh_registers = header.registers.size(); + header.shader.specials = &header.specials; + header.specials.dispatch_modifier = 0x8000; + header.registers = {{{0x20c, static_cast(address >> 8u)}, {0x20d, static_cast(address >> 40u)}, {0x207, 1}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 2}}}; + }; + fillHeader(pendingHeader, pendingCode.data(), sizeof(pendingCode)); + fillHeader(lazyHeader, lazyCode.data(), sizeof(lazyCode)); + AgcDriverRegisterShader_nid_postfix(&pendingHeader.shader); + AgcDriverRegisterShader_nid_postfix(&lazyHeader.shader); + AgcDriver::DriverDetail::AwaitRegistrationPreparation(); + const auto registry = AgcDriver::DriverDetail::Driver::Get().RegisteredShaders(); + Require(registry != nullptr, "dispatch ahead prewarm test published no shader registry"); + const auto pendingSnapshot = AgcDriver::DriverDetail::RegisteredHeader(*registry, &pendingHeader.shader); + const auto lazySnapshot = AgcDriver::DriverDetail::RegisteredHeader(*registry, &lazyHeader.shader); + Require(pendingSnapshot != nullptr && lazySnapshot != nullptr, "dispatch ahead prewarm test published no snapshots"); + { + std::lock_guard lock(pendingSnapshot->prepared->mutex); + pendingSnapshot->prepared->entries.clear(); + pendingSnapshot->prepared->demanded.store(false, std::memory_order_release); + pendingSnapshot->prepared->pending = true; + } + { + std::lock_guard lock(lazySnapshot->prepared->mutex); + lazySnapshot->prepared->entries.clear(); + lazySnapshot->prepared->pending = false; + } + alignas(256) static const std::array invalidCode{0xbf9f0000u}; + const auto invalidAddress = reinterpret_cast(invalidCode.data()); + const auto lazyOffsetAddress = reinterpret_cast(lazyCode.data() + 64); + alignas(16) const std::array indirectPairs{0x20cu, static_cast(lazyOffsetAddress >> 8u), 0x20du, static_cast(lazyOffsetAddress >> 40u), 0x240u, 7u}; + const auto pairsAddress = reinterpret_cast(indirectPairs.data()); + std::vector commands; + for (const auto reg : pendingHeader.registers) commands.insert(commands.end(), {0xc0017600u, reg.offset, reg.value}); + commands.insert(commands.end(), {0xc0017600u, 0x20cu, static_cast(invalidAddress >> 8u), 0xc0017600u, 0x20du, static_cast(invalidAddress >> 40u), 0xc0031500u, 1, 1, 1, 0x8041u, 0xffff1000u}); + commands.insert(commands.end(), {0xc0017600u, 0x20cu, pendingHeader.registers[0].value, 0xc0017600u, 0x20du, pendingHeader.registers[1].value, 0xc0031500u, 1, 1, 1, 0x8041u}); + commands.insert(commands.end(), {0xc0036300u, static_cast(pairsAddress), static_cast(pairsAddress >> 32u), 0x80000000u, 3u}); + commands.insert(commands.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x20, &packet) == 0, "dispatch ahead prewarm submit failed"); + ExpectFailure([] { AgcDriverWaitIdle_nid_postfix(); }, "s_code_end"); + AgcDriver::DriverDetail::AwaitRegistrationPreparation(); + AgcDriver::DriverDetail::AwaitLibraryPrebuilds(); + Require(pendingSnapshot->prepared->demanded.load(std::memory_order_acquire), "dispatch ahead did not prioritize the pending shader"); + { + std::lock_guard lock(lazySnapshot->prepared->mutex); + Require(lazySnapshot->prepared->entries.size() == 1 && lazySnapshot->prepared->entries.front().codeOffset == 64 && lazySnapshot->prepared->entries.front().handle != nullptr, "dispatch ahead did not prewarm the lazy shader for its dispatched offset"); + } + ExpectFailure([] { AgcDriverShutdown_nid_postfix(); }, "s_code_end"); +} + } int main(int argc, char** argv) { try { const std::string_view abiMode = argc == 2 ? argv[1] : ""; const bool abiDeferred = abiMode == "--abi-deferred-compute" || abiMode == "--abi-deferred-concurrent" || abiMode == "--abi-deferred-pixel" || abiMode == "--abi-deferred-stages" || abiMode == "--abi-deferred-rectangle" || abiMode == "--abi-deferred-undecodable-state" || abiMode == "--abi-deferred-registration-failure" || abiMode == "--abi-deferred-vertex-inputs" || abiMode == "--abi-deferred-rectangle-pairs"; - Require(argc == 1 || abiDeferred || (argc == 2 && (std::string_view(argv[1]) == "--indirect" || std::string_view(argv[1]) == "--fail-before-registration" || std::string_view(argv[1]) == "--deferred" || std::string_view(argv[1]) == "--deferred-undecodable" || std::string_view(argv[1]) == "--registered-workgroup" || std::string_view(argv[1]) == "--lazy")), "invalid test arguments"); + Require(argc == 1 || abiDeferred || (argc == 2 && (std::string_view(argv[1]) == "--indirect" || std::string_view(argv[1]) == "--fail-before-registration" || std::string_view(argv[1]) == "--deferred" || std::string_view(argv[1]) == "--deferred-undecodable" || std::string_view(argv[1]) == "--registered-workgroup" || std::string_view(argv[1]) == "--dispatch-ahead-prewarm" || std::string_view(argv[1]) == "--lazy")), "invalid test arguments"); if (abiDeferred) { if (!OpenVulkanTestDevice()) return VulkanTestSkipped; if (abiMode == "--abi-deferred-compute") AbiDeferredCompute(); @@ -1447,6 +1515,12 @@ int main(int argc, char** argv) { AgcDriverShutdown_nid_postfix(); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--dispatch-ahead-prewarm") { + if (!OpenVulkanTestDevice()) return VulkanTestSkipped; + DispatchAheadPrewarm(); + std::cout << "dispatch ahead prewarm tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--deferred") { if (!OpenVulkanTestDevice()) return VulkanTestSkipped; DeferredRegistration(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PresentKeyWriterWait.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PresentKeyWriterWait.cpp new file mode 100644 index 0000000000..e85b6e55d0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PresentKeyWriterWait.cpp @@ -0,0 +1,202 @@ +#include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Side = 128; +constexpr std::uint64_t DisplayFormat = 0x8000000000000000ull; +constexpr std::size_t SurfaceBytes = 65536; +constexpr std::size_t KeysOffset = SurfaceBytes; +constexpr std::size_t KeysCapacity = 32768; +constexpr std::size_t CopiedKeys = 256; +constexpr std::size_t SourceKeysOffset = KeysOffset + KeysCapacity; +constexpr std::size_t BlockBytes = 2 * 65536; +constexpr std::uint8_t Uncompressed = 0xff; +constexpr std::uint8_t Cleared = 0x00; + +alignas(256) constexpr std::array CopyKeysCode{ + 0x34020083, 0xe0341000, 0x80000201, 0xbf8c3f70, 0xe0741000, 0x80010201, 0xbf810000, +}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(65536, BlockBytes)); +#endif + Require(block != nullptr, "present key writer wait: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = AddressOf(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, const std::vector& userData) { + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +struct Scenario { + std::uint8_t* surface; + std::uint8_t* keys; + std::uint8_t* source; + std::size_t keyBytes; + AgcDriver::DisplayBuffer buffer; + std::vector copyData; +}; + +Scenario Prepare(std::uint8_t* block) { + Scenario scenario{block, block + KeysOffset, block + SourceKeysOffset, 0, {AddressOf(block), DisplayFormat, Side, Side}, std::vector(16, 0u)}; + scenario.buffer.dccAddress = AddressOf(scenario.keys); + scenario.keyBytes = AgcDriver::DisplayBufferKeyBytes(scenario.buffer); + Require(AgcDriver::DisplayBufferSize(scenario.buffer) <= SurfaceBytes && scenario.keyBytes >= CopiedKeys && scenario.keyBytes <= KeysCapacity, "present key writer wait: the display buffer does not fit the guest block"); + const auto source = BufferDescriptor(scenario.source, CopiedKeys); + const auto destination = BufferDescriptor(scenario.keys, CopiedKeys); + std::copy(source.begin(), source.end(), scenario.copyData.begin()); + std::copy(destination.begin(), destination.end(), scenario.copyData.begin() + 4); + return scenario; +} + +void Reset(const Scenario& scenario) { + std::memset(scenario.surface, 0x55, SurfaceBytes); + std::memset(scenario.keys, Uncompressed, scenario.keyBytes); + std::memset(scenario.keys, Cleared, CopiedKeys); + std::memset(scenario.source, Uncompressed, CopiedKeys); + AgcDriver::GuestMemory::BumpCollectEpoch(); +} + +std::string PresentFailure(AgcDriver::VulkanDevice& device, const AgcDriver::DisplayBuffer& buffer) { + try { + device.PresentDisplayBuffer(buffer); + } catch (const std::exception& error) { + return error.what(); + } + return "the headless present succeeded"; +} + +const std::string NoSwapchain = "Vulkan presentation: device has no swapchain"; + +bool CanCopyKeys(AgcDriver::VulkanDevice& device, const Scenario& scenario) { + const std::array pattern{0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu}; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const bool filled = device.FillBuffer(AddressOf(scenario.keys), CopiedKeys, pattern); + device.WaitIdle(); + return filled; +} + +void RunPendingWriter(AgcDriver::VulkanDevice& device, const Scenario& scenario) { + Reset(scenario); + const auto copy = Compile(device, CopyKeysCode, scenario.copyData); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Require(AgcDriver::GuestMemory::GpuMutex().DepthOnThisThread() == 1, "present key writer wait: the presenter's hold is not the outermost"); + device.Dispatch(copy, 1, 1, 1, {}, reinterpret_cast(CopyKeysCode.data())); + Require(AgcDriver::Graphics::DccKeyWritersPending(scenario.buffer.dccAddress, scenario.keyBytes), "a recorded kernel that copies data over the display keys is not seen as a pending key writer"); + const auto holds = AgcDriver::GuestMemory::GpuHoldSerial(); + const auto failure = PresentFailure(device, scenario.buffer); + const auto acquired = AgcDriver::GuestMemory::GpuHoldSerial() - holds; + Require(failure == NoSwapchain, "a present behind a pending writer of the display keys stopped before the swapchain: " + failure); + Require(acquired != 0, "the presenter synchronized with the display keys' writer without releasing the GPU mutex"); + Require(!AgcDriver::Graphics::DccKeyWritersPending(scenario.buffer.dccAddress, scenario.keyBytes), "the display keys' writer is still pending after the present"); + Require(scenario.keys[0] == Uncompressed && scenario.keys[CopiedKeys - 1] == Uncompressed, "the present read the display keys before their writer landed"); +} + +void RunNoWriter(AgcDriver::VulkanDevice& device, const Scenario& scenario) { + std::memset(scenario.keys, Uncompressed, scenario.keyBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.WaitIdle(); + Require(!AgcDriver::Graphics::DccKeyWritersPending(scenario.buffer.dccAddress, scenario.keyBytes), "the idle device still reports a pending writer of the display keys"); + const auto holds = AgcDriver::GuestMemory::GpuHoldSerial(); + const auto failure = PresentFailure(device, scenario.buffer); + const auto acquired = AgcDriver::GuestMemory::GpuHoldSerial() - holds; + Require(failure == NoSwapchain, "a present with no pending writer of the display keys stopped before the swapchain: " + failure); + Require(acquired == 0, "a present with no pending writer of the display keys released the GPU mutex"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!device->CanWaitUnlocked()) { + std::puts("skipped, the device has no timeline semaphores: the presenter cannot wait without the GPU mutex"); + return VulkanTestSkipped; + } + GuestBlock block; + const auto scenario = Prepare(block.Data()); + if (!CanCopyKeys(*device, scenario)) { + std::puts("skipped, host imports are unavailable: the key writer of the test is a copied writer"); + return VulkanTestSkipped; + } + RunPendingWriter(*device, scenario); + RunNoWriter(*device, scenario); + std::puts("present key writer wait tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/QueuedIndexReadOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/QueuedIndexReadOrder.cpp index acc2b1617a..d69048386f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/QueuedIndexReadOrder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/QueuedIndexReadOrder.cpp @@ -343,10 +343,8 @@ void CopyPendingImage(std::size_t bytes, const char* phase) { int main() { #ifdef _WIN32 _putenv_s("APS5_PIPELINED_DRAWS", "1"); - _putenv_s("APS5_PIPELINE_DMA", "1"); #else setenv("APS5_PIPELINED_DRAWS", "1", 1); - setenv("APS5_PIPELINE_DMA", "1", 1); #endif try { { @@ -373,6 +371,11 @@ int main() { AgcDriverWaitIdle_nid_postfix(); Require(AgcDriver::Graphics::StorageTexture::AnyPending(), "queued index read: the image write left no pending result to order against"); Require(IndicesIntact(), "queued index read: the image's results reached guest memory before any consumer asked"); + const auto copiedAddress = static_cast(reinterpret_cast(Copied)); + const auto heldQueries = AgcDriver::DriverDetail::DrawPipeline::HeldReadQueries(); + Require(!AgcDriver::Graphics::StorageTexture::FlushPending(copiedAddress, PoolBytes), "queued index read: a flush over memory without pending results stored something"); + Require(AgcDriver::DriverDetail::DrawPipeline::HeldReadQueries() == heldQueries, "queued index read: a flush with nothing to store asked the draw pipeline for held reads"); + Require(IndicesIntact(), "queued index read: a flush over other memory stored the image's results"); GateUntilQueued(); std::fill_n(ColorMemory, ColorBytes, std::byte{0}); Submit(Draw(Vertices.data(), Vertices.size()), "the indexed draw"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index 69392bd173..5870d759ac 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -1015,6 +1015,20 @@ int main(int argc, char** argv) { Block stored(BlockBytes); StoreWords(stored, colorBytes, FillOnes); CheckUniform(LoadSamples(*device, stored), Ones, "from color memory of half-float ones no draw rendered to"); + Block reused(BlockBytes); + StoreWords(reused, colorBytes, FillOnes); + CheckUniform(LoadSamples(*device, reused), Ones, "from color memory of half-float ones no draw rendered to, before the title reuses it"); + AgcDriver::Graphics::RequireNoMultisampleColorTarget(reused.Address(), static_cast(colorBytes / 2u), "a storage image access"); + StoreWords(reused, colorBytes, 0u); + CheckUniform(LoadSamples(*device, reused), {}, "from color memory no draw rendered to after the title reused it for zeros"); + std::string drawnRefusal; + try { + AgcDriver::Graphics::RequireNoMultisampleColorTarget(block.Address(), static_cast(colorBytes / 2u), "a storage image access"); + } catch (const std::exception& error) { + drawnRefusal = error.what(); + } + Require(drawnRefusal.find("whose samples are only on the host") != std::string::npos, "a guest access over a drawn 4x target is not refused"); + std::puts("a guest access over a 4x texture no draw rendered to retires it, so a later load reads the memory's new value, and a guest access over a drawn target is still refused"); const auto storedState = TargetState(DecodeTarget(stored)); Draw(*device, storedState); CheckSamples(storedState, LoadSamples(*device, stored), Ones); @@ -1031,6 +1045,23 @@ int main(int argc, char** argv) { Block undrawn(BlockBytes); StoreSamples(*device, TargetDescriptor(undrawn, SampleShift), SampleWrites(leftHalf)); CheckWritten(std::vector>(Side * Side * Samples), LoadSamples(*device, undrawn), leftHalf, "into zeroed color memory no draw rendered to"); + const auto refusedOver = [&](const Block& over) { + try { + AgcDriver::Graphics::RequireNoMultisampleColorTarget(over.Address(), static_cast(colorBytes / 2u), "a storage image access"); + } catch (const std::exception& error) { + return std::string(error.what()).find("whose samples are only on the host") != std::string::npos; + } + return false; + }; + Require(refusedOver(undrawn), "a guest access over a 4x texture no draw rendered to but a dispatch wrote is not refused"); + Block absorbing(BlockBytes); + StoreWords(absorbing, colorBytes, FillOnes); + CheckUniform(LoadSamples(*device, absorbing), Ones, "from color memory of half-float ones no draw rendered to, before a fill the texture absorbs"); + Require(AgcDriver::Graphics::MultisampledColorFillAbsorbs(absorbing.Address(), static_cast(colorBytes), 0u), "a fill of 0 over the whole color memory of a 4x texture no draw rendered to is not absorbed"); + AgcDriver::Graphics::NoteColorMetadataFill(absorbing.Address(), static_cast(colorBytes), 0u); + Require(refusedOver(absorbing), "a guest access over a 4x texture no draw rendered to that absorbed a fill is not refused"); + CheckUniform(LoadSamples(*device, absorbing), {}, "from a 4x texture no draw rendered to after it absorbed a fill of 0"); + std::puts("a guest access over a 4x texture no draw rendered to is still refused once a dispatch wrote it or it absorbed a fill"); Block mixedStore(BlockBytes); StoreWords(mixedStore, colorBytes, FillOnes); std::memcpy(mixedStore.data + colorBytes - sizeof(odd), &odd, sizeof(odd)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SdwaCvtF32Ubyte.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SdwaCvtF32Ubyte.cpp index 4468d5d220..dac88be437 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SdwaCvtF32Ubyte.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SdwaCvtF32Ubyte.cpp @@ -17,20 +17,50 @@ using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Threads = 64; constexpr std::uint32_t Inputs = 4; constexpr std::uint32_t Results = 4; +constexpr std::uint32_t Selectors = 7; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(256) constexpr std::array SdwaCode{ +constexpr std::array ModifierWords{14, 16, 18, 20}; + +alignas(256) std::array SdwaCode{ 0x34020082, 0x34060082, 0xe0302000, 0x80000401, 0x7e1402ff, 0xabcd1234, 0x7e1602ff, 0xabcd1234, 0x7e1802ff, 0xabcd1234, 0x7e1a02ff, 0xabcd1234, - 0xbf8c3f70, 0x7e1424f9, 0x00040604, 0x7e1624f9, 0x00050604, 0x7e1826f9, 0x000d0604, 0x7e1a28f9, 0x000b0604, + 0xbf8c3f70, 0x7e1422f9, 0x00060604, 0x7e1624f9, 0x00060604, 0x7e1826f9, 0x00060604, 0x7e1a28f9, 0x00060604, 0xe0702000, 0x80010a03, 0xe0702004, 0x80010b03, 0xe0702008, 0x80010c03, 0xe070200c, 0x80010d03, 0xbf810000, }; +const std::array SelectorNames{"BYTE_0", "BYTE_1", "BYTE_2", "BYTE_3", "WORD_0", "WORD_1", "DWORD"}; + +struct SelectVector { + std::uint32_t value; + std::array zeroExtended; + std::array signExtended; +}; + +constexpr std::array Vectors{{ + {0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0xffffffffu, {0x000000ffu, 0x000000ffu, 0x000000ffu, 0x000000ffu, 0x0000ffffu, 0x0000ffffu, 0xffffffffu}, {0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu}}, + {0x12345678u, {0x00000078u, 0x00000056u, 0x00000034u, 0x00000012u, 0x00005678u, 0x00001234u, 0x12345678u}, {0x00000078u, 0x00000056u, 0x00000034u, 0x00000012u, 0x00005678u, 0x00001234u, 0x12345678u}}, + {0x80808080u, {0x00000080u, 0x00000080u, 0x00000080u, 0x00000080u, 0x00008080u, 0x00008080u, 0x80808080u}, {0xffffff80u, 0xffffff80u, 0xffffff80u, 0xffffff80u, 0xffff8080u, 0xffff8080u, 0x80808080u}}, + {0x7f7f7f7fu, {0x0000007fu, 0x0000007fu, 0x0000007fu, 0x0000007fu, 0x00007f7fu, 0x00007f7fu, 0x7f7f7f7fu}, {0x0000007fu, 0x0000007fu, 0x0000007fu, 0x0000007fu, 0x00007f7fu, 0x00007f7fu, 0x7f7f7f7fu}}, + {0x8182a3f4u, {0x000000f4u, 0x000000a3u, 0x00000082u, 0x00000081u, 0x0000a3f4u, 0x00008182u, 0x8182a3f4u}, {0xfffffff4u, 0xffffffa3u, 0xffffff82u, 0xffffff81u, 0xffffa3f4u, 0xffff8182u, 0x8182a3f4u}}, + {0x7f00ff80u, {0x00000080u, 0x000000ffu, 0x00000000u, 0x0000007fu, 0x0000ff80u, 0x00007f00u, 0x7f00ff80u}, {0xffffff80u, 0xffffffffu, 0x00000000u, 0x0000007fu, 0xffffff80u, 0x00007f00u, 0x7f00ff80u}}, + {0x00008000u, {0x00000000u, 0x00000080u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u, 0x00008000u}, {0x00000000u, 0xffffff80u, 0x00000000u, 0x00000000u, 0xffff8000u, 0x00000000u, 0x00008000u}}, + {0x0000007fu, {0x0000007fu, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000007fu, 0x00000000u, 0x0000007fu}, {0x0000007fu, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000007fu, 0x00000000u, 0x0000007fu}}, + {0x00000080u, {0x00000080u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000080u, 0x00000000u, 0x00000080u}, {0xffffff80u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000080u, 0x00000000u, 0x00000080u}}, + {0x80000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000080u, 0x00000000u, 0x00008000u, 0x80000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0xffffff80u, 0x00000000u, 0xffff8000u, 0x80000000u}}, + {0x00800000u, {0x00000000u, 0x00000000u, 0x00000080u, 0x00000000u, 0x00000000u, 0x00000080u, 0x00800000u}, {0x00000000u, 0x00000000u, 0xffffff80u, 0x00000000u, 0x00000000u, 0x00000080u, 0x00800000u}}, + {0x0000ff00u, {0x00000000u, 0x000000ffu, 0x00000000u, 0x00000000u, 0x0000ff00u, 0x00000000u, 0x0000ff00u}, {0x00000000u, 0xffffffffu, 0x00000000u, 0x00000000u, 0xffffff00u, 0x00000000u, 0x0000ff00u}}, + {0xdeadbeefu, {0x000000efu, 0x000000beu, 0x000000adu, 0x000000deu, 0x0000beefu, 0x0000deadu, 0xdeadbeefu}, {0xffffffefu, 0xffffffbeu, 0xffffffadu, 0xffffffdeu, 0xffffbeefu, 0xffffdeadu, 0xdeadbeefu}}, + {0x01020304u, {0x00000004u, 0x00000003u, 0x00000002u, 0x00000001u, 0x00000304u, 0x00000102u, 0x01020304u}, {0x00000004u, 0x00000003u, 0x00000002u, 0x00000001u, 0x00000304u, 0x00000102u, 0x01020304u}}, + {0xf0e0d0c0u, {0x000000c0u, 0x000000d0u, 0x000000e0u, 0x000000f0u, 0x0000d0c0u, 0x0000f0e0u, 0xf0e0d0c0u}, {0xffffffc0u, 0xffffffd0u, 0xffffffe0u, 0xfffffff0u, 0xffffd0c0u, 0xfffff0e0u, 0xf0e0d0c0u}}, +}}; + void FillInput() { for (std::uint32_t tid = 0; tid < Threads; ++tid) { - Input[tid * Inputs] = tid | ((tid + 64u) << 8u) | ((255u - tid) << 16u) | (((tid * 3u + 128u) & 0xffu) << 24u); + Input[tid * Inputs] = Vectors[tid % Vectors.size()].value; } } @@ -49,7 +79,10 @@ std::array BufferDescriptor(const void* data, std::uint32_t co return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), count, 0x11016facu}; } -void Run(AgcDriver::VulkanDevice& device) { +void Run(AgcDriver::VulkanDevice& device, std::uint32_t selector, bool signExtend) { + for (const auto word : ModifierWords) { + SdwaCode[word] = 0x00000604u | (selector << 16u) | (signExtend ? 1u << 19u : 0u); + } Output.fill(0xdeadbeefu); std::vector userData(8, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(Input.size())); @@ -71,18 +104,16 @@ void Run(AgcDriver::VulkanDevice& device) { device.WaitIdle(); } -void Check() { +void Check(std::uint32_t selector, bool signExtend) { for (std::uint32_t tid = 0; tid < Threads; ++tid) { - const auto source = Input[tid * Inputs]; + const auto& vector = Vectors[tid % Vectors.size()]; + const auto selected = signExtend ? vector.signExtended[selector] : vector.zeroExtended[selector]; const auto* out = &Output[tid * Results]; - const auto where = [&](std::uint32_t result) { return "sdwa cvt f32 ubyte: thread " + std::to_string(tid) + " result " + std::to_string(result) + " is " + Hex(out[result]); }; - const auto byte1 = FloatBits((source >> 8u) & 0xffu); - const auto byte3 = FloatBits(source >> 24u); - const auto signFill = FloatBits((source & 0x80000000u) != 0u ? 0xffu : 0u); - Require(out[0] == byte1, where(0) + ", expected " + Hex(byte1) + ": v_cvt_f32_ubyte1 src0_sel:WORD_0 must convert bits 15:8"); - Require(out[1] == byte3, where(1) + ", expected " + Hex(byte3) + ": v_cvt_f32_ubyte1 src0_sel:WORD_1 must convert bits 31:24"); - Require(out[2] == signFill, where(2) + ", expected " + Hex(signFill) + ": v_cvt_f32_ubyte2 src0_sel:WORD_1 src0_sext must convert the sign extension of the high word"); - Require(out[3] == signFill, where(3) + ", expected " + Hex(signFill) + ": v_cvt_f32_ubyte3 src0_sel:BYTE_3 src0_sext must convert the sign extension of the top byte"); + for (std::uint32_t byte = 0; byte < Results; ++byte) { + const auto expected = FloatBits((selected >> (byte * 8u)) & 0xffu); + Require(out[byte] == expected, "sdwa v_cvt_f32_ubyte" + std::to_string(byte) + " src0_sel:" + SelectorNames[selector] + (signExtend ? " src0_sext" : "") + " of " + + Hex(vector.value) + " is " + Hex(out[byte]) + ", expected " + Hex(expected)); + } } } @@ -93,8 +124,12 @@ int main() { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; FillInput(); - Run(*device); - Check(); + for (const bool signExtend : {false, true}) { + for (std::uint32_t selector = 0; selector < Selectors; ++selector) { + Run(*device, selector, signExtend); + Check(selector, signExtend); + } + } std::puts("sdwa cvt f32 ubyte tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SkipShaderFile.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SkipShaderFile.cpp new file mode 100644 index 0000000000..cec7c1cc4c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SkipShaderFile.cpp @@ -0,0 +1,102 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +alignas(256) constexpr std::array First{0xd7460000u, 0x04010c04u, 0x7e0202ffu, 0x12345678u, 0xe0702000u, 0x80000100u, 0xbf810000u}; +alignas(256) constexpr std::array Second{0xd7460000u, 0x04010c04u, 0x7e0202ffu, 0x12345678u, 0xe0702000u, 0x80000100u, 0xbf810000u}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +constexpr std::uint32_t Stored = 0x12345678u; +alignas(256) std::array FirstOutput{}; +alignas(256) std::array SecondOutput{}; + +void Dispatch(const std::array& code, std::array& output) { + const auto address = reinterpret_cast(output.data()); + const std::array data{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), static_cast(output.size()), 0x00014204u}; + const auto program = reinterpret_cast(code.data()); + std::vector words{ + 0xc0037600u, 0x207, 64, 1, 1, + 0xc0027600u, 0x20c, static_cast(program >> 8u), static_cast(program >> 40u), + 0xc0017600u, 0x213, (4u << 1u) | (1u << 7u), + 0xc0047600u, 0x240, data[0], data[1], data[2], data[3], + 0xc0031500u, 1, 1, 1, 0x41u, + }; + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(address, sizeof(output)); +} + +std::string Hex(const void* address) { + char text[32]; + std::snprintf(text, sizeof(text), "%llx", static_cast(reinterpret_cast(address))); + return text; +} + +void List(const std::filesystem::path& path, const std::string& contents) { + std::ofstream(path, std::ios::trunc) << contents; + std::this_thread::sleep_for(std::chrono::milliseconds(1100)); +} + +void Run(const char* phase, bool firstSkipped, bool secondSkipped) { + FirstOutput.fill(Untouched); + SecondOutput.fill(Untouched); + Dispatch(First, FirstOutput); + Dispatch(Second, SecondOutput); + for (std::size_t i = 0; i < FirstOutput.size(); ++i) { + Require(FirstOutput[i] == (firstSkipped ? Untouched : Stored), std::string(phase) + ": the first shader's dispatch " + (firstSkipped ? "stored" : "did not store") + " dword " + std::to_string(i)); + Require(SecondOutput[i] == (secondSkipped ? Untouched : Stored), std::string(phase) + ": the second shader's dispatch " + (secondSkipped ? "stored" : "did not store") + " dword " + std::to_string(i)); + } +} + +} + +int main() { + const auto path = std::filesystem::temp_directory_path() / ("aps5-skip-shader-file-" + Hex(&FirstOutput) + ".txt"); + try { + List(path, "# listed\n" + Hex(First.data()) + "\n"); +#ifdef _WIN32 + Require(_putenv_s("APS5_SKIP_SHADER_FILE", path.string().c_str()) == 0, "cannot set APS5_SKIP_SHADER_FILE"); +#else + Require(setenv("APS5_SKIP_SHADER_FILE", path.string().c_str(), 1) == 0, "cannot set APS5_SKIP_SHADER_FILE"); +#endif + { + const auto device = OpenVulkanTestDevice(); + if (!device) { + std::filesystem::remove(path); + return VulkanTestSkipped; + } + } + Run("first listed", true, false); + List(path, Hex(Second.data()) + " # the first is not listed any more\n"); + Run("second listed", false, true); + List(path, Hex(First.data()) + "zz 1" + std::string(16, '0') + " 0x" + Hex(Second.data()) + "\n"); + Run("malformed and overflowing tokens", false, true); + List(path, ""); + Run("none listed", false, false); + AgcDriverShutdown_nid_postfix(); + std::filesystem::remove(path); + std::puts("skip shader file tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + std::error_code ignored; + std::filesystem::remove(path, ignored); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilLogicOperations.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilLogicOperations.cpp new file mode 100644 index 0000000000..21f62c48d6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilLogicOperations.cpp @@ -0,0 +1,202 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.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 = 16; +constexpr std::uint32_t WaveSize = 64; +constexpr std::uint32_t PlainControl = 0x50u; +constexpr std::byte Kept{0x40}; +alignas(256) std::array Pixels{}; +alignas(256) std::array DepthMemory{}; +alignas(256) std::array StencilMemory{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array, 3> Vertices{{ + {-1.0f, -1.0f, 0.25f, 1.0f}, {3.0f, -1.0f, 0.25f, 1.0f}, {-1.0f, 3.0f, 0.25f, 1.0f}, +}}; + +struct Shaders { + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult pixel; + std::uint32_t pixelPush = 0; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +ShaderRecompiler::ShaderPixelStageInfo PixelInfo() { + const AgcDriver::Registers context{ + {0x1b3, 0x700u}, {0x1b4, 0x700u}, {0x1b6, 0u}, {0x1c5, 9u}, {0x203, PlainControl}, + }; + return AgcDriver::Graphics::DecodePixelStageInfo(context, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}); +} + +Shaders Compile(AgcDriver::VulkanDevice& device) { + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = 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 vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + Shaders shaders{ShaderRecompiler::Recompile(vertex), {}, 0}; + const auto vertexPush = static_cast(shaders.vertex.pushConstants.size()); + std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, PixelInfo(), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + shaders.pixel = ShaderRecompiler::Recompile(fragment); + shaders.pixelPush = PixelPushOffset(vertexPush, target); + return shaders; +} + +AgcDriver::QueueState StencilQueue(std::uint32_t ops, std::uint32_t refMask, std::uint32_t depthControl) { + AgcDriver::QueueState queue; + queue.userConfig[0x242] = 4; + queue.context = { + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x1b8, 0}, {0x207, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x000, 0}, {0x002, 0}, {0x007, (1u << 16u) | 3u}, {0x00a, 0}, {0x00b, 0}, {0x010, 0x22900983u}, {0x011, 0x20000181u}, + {0x012, 0x100}, {0x014, 0x100}, {0x013, 0x200}, {0x015, 0x200}, {0x005, 0x300}, {0x01e, 0x1}, + {0x10b, ops}, {0x10c, refMask}, {0x10d, refMask}, {0x200, depthControl} + }; + const auto address = reinterpret_cast(Pixels.data()); + queue.context[0x318] = static_cast(address >> 8u); + queue.context[0x390] = static_cast(address >> 40u); + queue.context[0x10f] = std::bit_cast(32.0f); + queue.context[0x110] = std::bit_cast(32.0f); + queue.context[0x111] = std::bit_cast(-2.0f); + queue.context[0x112] = std::bit_cast(2.0f); + queue.context[0x113] = std::bit_cast(1.0f); + queue.context[0x114] = 0; + queue.context[0xb4] = 0; + queue.context[0xb5] = std::bit_cast(1.0f); + queue.shader[0x008] = 0x100; + queue.shader[0x009] = 0; + return queue; +} + +void Draw(AgcDriver::VulkanDevice& device, const Shaders& shaders, const VkStencilOpState& stencil) { + Pixels.fill(Kept); + const std::array stages{{ + {ShaderStage::Vertex, &shaders.vertex, 0}, + {ShaderStage::Fragment, &shaders.pixel, shaders.pixelPush} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(DepthMemory.data()), reinterpret_cast(StencilMemory.data()), {Width, Height}, VK_FORMAT_D32_SFLOAT_S8_UINT, 1.0f, 0}; + state.depthTest = false; + state.depthCompare = VK_COMPARE_OP_ALWAYS; + state.stencilTest = true; + state.stencilFront = stencil; + state.stencilBack = stencil; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + device.Draw(state, AgcDriver::Pm4::DrawParameters{0, 3u, 0, 1u, 0, false}, stages); + device.WaitIdle(); +} + +std::uint32_t Expected(std::uint32_t op, std::uint32_t stored, std::uint32_t value, std::uint32_t writeMask) { + const auto result = op == 10u ? stored & value : op == 11u ? stored | value : op == 12u ? stored ^ value : ~(stored ^ value); + return ((stored & ~writeMask) | (result & writeMask)) & 0xffu; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto shaders = Compile(*device); + const auto replace = [](std::uint32_t value) { return VkStencilOpState{VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_REPLACE, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, value}; }; + const auto equal = [](std::uint32_t value) { return VkStencilOpState{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, 0xffu, 0u, value}; }; + std::size_t checked = 0; + for (const auto stored : {0xa5u, 0x3cu}) { + for (const auto op : {10u, 11u, 12u, 15u}) { + for (const auto [writeMask, value] : {std::pair{0xffu, 0x3cu}, std::pair{0x0fu, 0x96u}, std::pair{0xf6u, 0xffu}}) { + for (const auto [ops, compare] : {std::pair{op << 4u, 7u}, std::pair{op, 0u}}) { + const auto refMask = 0xff00u | (writeMask << 16u) | (value << 24u); + const auto face = AgcDriver::Graphics::DecodeState(StencilQueue(ops, refMask, 1u | (compare << 8u))).stencilFront; + const auto expected = Expected(op, stored, value, writeMask); + const auto name = "stencil operation " + std::to_string(op) + " in DB_STENCIL_CONTROL " + std::to_string(ops) + " with DB_STENCILREFMASK " + std::to_string(refMask) + " on stored value " + std::to_string(stored); + Draw(*device, shaders, replace(stored)); + Draw(*device, shaders, face); + Draw(*device, shaders, equal(expected)); + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + Require(Pixels[pixel * 4u] == std::byte{255}, name + ": pixel " + std::to_string(pixel) + " does not hold the expected stencil value " + std::to_string(expected)); + } + Draw(*device, shaders, equal(expected ^ 0x80u)); + Require(std::ranges::all_of(std::span(Pixels).first(4), [](std::byte value) { return value == Kept; }), name + ": the stencil value also equals " + std::to_string(expected ^ 0x80u)); + ++checked; + } + } + } + } + std::printf("stencil logic operation tests passed: %zu states\n", checked); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp index 3612fae6ef..14d571cf48 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp @@ -88,7 +88,7 @@ ShaderRecompiler::ShaderPixelStageInfo PixelInfo(std::uint32_t shaderControl) { return AgcDriver::Graphics::DecodePixelStageInfo(context, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}); } -void Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, std::uint32_t shaderControl, const VkStencilOpState& stencil, std::uint32_t vertices = 3u, std::uint32_t instances = 1u, bool depthFail = false, const std::function& alter = {}, const std::function& alterDraw = {}, std::span> vertexData = Vertices, std::span vertexCode = VertexCode, std::span pixelData = {}, std::span pixelSrt = {}, const std::function& alterVertex = {}) { +void Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, std::uint32_t shaderControl, const VkStencilOpState& stencil, std::uint32_t vertices = 3u, std::uint32_t instances = 1u, bool depthFail = false, const std::function& alter = {}, const std::function& alterDraw = {}, std::span> vertexData = Vertices, std::span vertexCode = VertexCode, std::span pixelData = {}, std::span pixelSrt = {}, const std::function& alterVertex = {}, std::uint32_t pixelPushBytes = 0u) { Pixels.fill(Kept); auto target = device.Target(); std::vector capabilities(target.supportedCapabilities.begin(), target.supportedCapabilities.end()); @@ -113,11 +113,13 @@ void Draw(AgcDriver::VulkanDevice& device, std::span pixelC auto pixelUserData = pixelData.empty() ? std::vector(8, 0u) : std::vector(pixelData.begin(), pixelData.end()); std::vector pixelMemory{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}; pixelMemory.insert(pixelMemory.end(), pixelSrt.begin(), pixelSrt.end()); + auto pixelLayout = PixelPushLayout(vertexPush, target); + if (pixelPushBytes != 0u) pixelLayout.pushConstantSizeBytes = pixelPushBytes; ShaderRecompiler::RecompileRequest fragment{ {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {WaveSize, 0, pixelUserData, std::nullopt, PixelInfo(shaderControl), std::nullopt, pixelMemory}, target, - PixelPushLayout(vertexPush, target) + pixelLayout }; fragment.useCache = false; const auto pixelResult = ShaderRecompiler::Recompile(fragment); @@ -350,6 +352,14 @@ int main() { const std::array colorData{0x3f800000u}; Draw(*device, userColor, exported, replace(), 3u, 1u, false, {}, {}, Vertices, VertexCode, colorData); Check(all, "dynamic reference preserves the adjacent shader-data push payload"); + auto bufferColor = std::vector(alternating.begin(), alternating.end()); + *std::find(bufferColor.begin(), bufferColor.end(), 0x7e0e02f2u) = 0x7e0e0208u; + bufferColor.insert(bufferColor.begin(), {0xf4200200u, 0xfa000000u, 0xbf8cc07fu}); + alignas(256) const std::array colorBuffer{0x3f800000u, 0u, 0u, 0u}; + const auto colorDescriptor = BufferDescriptor(colorBuffer.data(), 4u, static_cast(colorBuffer.size())); + const std::array colorMemory{{{reinterpret_cast(colorBuffer.data()), std::as_bytes(std::span(colorBuffer))}}}; + Draw(*device, bufferColor, exported, replace(), 3u, 1u, false, {}, {}, Vertices, VertexCode, colorDescriptor, colorMemory, {}, sizeof(std::uint32_t)); + Check(all, "dynamic reference keeps guest buffer offsets in shader data outside the push constants"); auto guarded = std::vector(alternating.begin(), alternating.end()); *std::find(guarded.begin(), guarded.end(), 0x7e0e02f2u) = 0x7e0e020cu; guarded.insert(guarded.begin(), {0xf4040280u, 0xfa000018u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu}); @@ -369,6 +379,29 @@ int main() { catch (const std::exception& error) { faultThrown = std::string(error.what()).find("BDA") != std::string::npos; } Require(faultThrown, "poisoned readonly SRT replay did not throw a host fault"); Require(std::ranges::all_of(Pixels, [](std::byte value) { return value == Kept; }), "poisoned replay published guest color results before its fault"); + alignas(256) std::array columnValues{}; + alignas(256) std::array columnTexels{}; + columnValues[0] = 0x2fu; + for (std::uint32_t x = 0; x < Width; ++x) { + columnValues[x + 1u] = 0x30u + x; + columnTexels[x] = 1.0f; + } + constexpr std::array bufferImageCode{ + 0x7e060f00u, 0x7e080280u, 0xf0001108u, 0x00010503u, 0xe0302000u, 0x80000603u, 0xe0302000u, 0x80030803u, + 0xbf8c3f70u, 0x7e0e02f2u, 0xf8000082u, 0x00000600u, 0xf800180fu, 0x07050805u, 0xbf810000u, + }; + const auto columnBuffer = BufferDescriptor(columnValues.data() + 1, 4u, Width); + const auto alignedBuffer = BufferDescriptor(columnValues.data(), 4u, Width + 1u); + const auto texelAddress = static_cast(reinterpret_cast(columnTexels.data())); + const std::array columnTexture{static_cast(texelAddress >> 8u), static_cast((texelAddress >> 40u) & 0xffu) | (22u << 20u) | (((Width - 1u) & 3u) << 30u), (Width - 1u) >> 2u, 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u}; + std::vector bufferImageData(columnBuffer.begin(), columnBuffer.end()); + bufferImageData.insert(bufferImageData.end(), columnTexture.begin(), columnTexture.end()); + bufferImageData.insert(bufferImageData.end(), alignedBuffer.begin(), alignedBuffer.end()); + Draw(*device, PlainPixelCode, plain, replace(0u)); + Draw(*device, bufferImageCode, exported, replace(), 3u, 1u, false, {}, {}, Vertices, VertexCode, bufferImageData); + Check(all, "a replayed export beside an image draws the image's texels"); + Draw(*device, PlainPixelCode, plain, equal(0x35u)); + Check([](std::uint32_t x, std::uint32_t) { return x == 5u; }, "a replayed export beside an image reads its guest buffer at a 4-byte offset"); std::puts("stencil replay tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageFileWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageFileWrite.cpp index 5b9fbc5ade..8658f5b45b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StorageFileWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageFileWrite.cpp @@ -308,8 +308,8 @@ void CheckShutdownAndGuard() { OutputFile file; std::vector source(TransferBytes, std::byte{0x5a}); AgcDriver::GuestMemory::SetFlushHook(nullptr); - AgcDriver::GuestMemory::SetReadGuard([](std::uint64_t, std::size_t) { throw std::runtime_error("file source read guard"); }); const auto descriptor = file.Open(); + AgcDriver::GuestMemory::SetReadGuard([](std::uint64_t, std::size_t) { throw std::runtime_error("file source read guard"); }); bool refused = false; try { sceKernelWrite(descriptor, source.data(), source.size()); } catch (const std::runtime_error& error) { refused = std::string(error.what()) == "file source read guard"; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/UintColorTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/UintColorTarget.cpp new file mode 100644 index 0000000000..9c9d7f8230 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/UintColorTarget.cpp @@ -0,0 +1,332 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.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 + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t ScissorWidth = 128; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; + +struct TargetFormat { + const char* name; + std::uint32_t info; + VkFormat format; + std::uint32_t channels; + std::uint32_t channelBytes; +}; + +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}; + +struct ExportCase { + const char* name; + std::uint32_t format; + VkColorComponentFlags writeMask; +}; + +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}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 128> Programs{}; +std::size_t programCount = 0; + +std::span Program(const std::array& vgprs, bool compressed) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + for (std::uint32_t vgpr = 0; vgpr < (compressed ? 2u : 4u); ++vgpr) { + code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); + code[at++] = vgprs[vgpr]; + } + code[at++] = compressed ? 0xf8001c0fu : 0xf800180fu; + code[at++] = compressed ? 0x00000504u : 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +std::span WordProgram(const std::array& words) { + return Program(words, false); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::size_t bytes, const TargetFormat& target, std::uint32_t attrib3) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = target.info; + cx[0x31d] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = attrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == 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"); + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, const ExportCase& exported, std::span pixelCode) { + const auto exportFormat = exported.format; + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(Triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {ScissorWidth, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = exported.writeMask; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, exportFormat}}; + 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}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto handle = ShaderRecompiler::PrepareShader(fragment); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}}; + snapshot.prepared->entries.push_back({0, handle}); + 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 auto materialize = [&](const ShaderRecompiler::RecompileRequest& request) { + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request); + return invocation.Materialize(*invocation.Capture(runtime)); + }; + const auto pixelResult = materialize(fragment); + auto unpacked = fragment; + unpacked.context.pixel->targetExportPacking.fill(ShaderRecompiler::ColorExportPacking::None); + 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"); + 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); +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, const Block& block) { + AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(block.bytes); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +std::uint32_t Channel(const std::vector& bytes, std::size_t at, std::uint32_t channelBytes) { + std::uint32_t value = 0; + std::memcpy(&value, bytes.data() + at, channelBytes); + return value; +} + +void ExpectSurface(AgcDriver::VulkanDevice& device, const Block& block, const TargetFormat& target, const ColorTargetLayout& layout, const std::vector& before, const ExportCase& exported, const std::array& expected, const std::string& what) { + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + for (std::uint32_t channel = 0; channel < target.channels; ++channel) { + const auto at = layout.Offset(x, y) + channel * target.channelBytes; + const bool written = x < ScissorWidth && (exported.writeMask & (1u << channel)) != 0; + const auto want = written ? expected[channel] : Channel(before, at, target.channelBytes); + const auto got = Channel(stored, at, target.channelBytes); + if (got == want) continue; + char detail[192]; + std::snprintf(detail, sizeof(detail), ": channel %u of pixel (%u, %u) %s holds 0x%08x, expected 0x%08x", channel, x, y, written ? "written" : "not written", got, want); + throw std::runtime_error(what + detail); + } + } + } +} + +std::vector Fill(Block& block) { + std::vector words(block.bytes / 4u); + for (std::size_t index = 0; index < words.size(); ++index) words[index] = static_cast(index * 0x9e3779b9u); + std::memcpy(block.data, words.data(), block.bytes); + return {block.data, block.data + block.bytes}; +} + +std::array Expected(const ExportCase& exported, const std::array& vgprs) { + if (exported.format == Export32AR.format) return {vgprs[0], 0u, 0u, vgprs[1]}; + return vgprs; +} + +void TargetTests(AgcDriver::VulkanDevice& device, const TargetFormat& target, const ExportCase& exported, std::span> values, ColorTileMode mode, const char* kind) { + 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); + auto before = Fill(block); + 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); + 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"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + constexpr std::array words{{0u, 1u, 0x3f800000u, 0x7fc00000u, 0xff800000u, 0x80000000u, 0xdeadbeefu, 0xffffffffu}}; + std::vector> single; + for (const auto word : words) single.push_back({word, 0x11111111u, 0x22222222u, 0x33333333u}); + FormatTests(*device, R32, Export32R, single); + std::vector> wide; + for (std::size_t index = 0; index < words.size(); ++index) wide.push_back({words[index], words[(index + 1) % words.size()], words[(index + 2) % words.size()], words[(index + 4) % words.size()]}); + FormatTests(*device, R32G32, Export32GR, wide); + FormatTests(*device, R32G32B32A32, Export32AR, wide); + 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); + std::puts("UINT color target 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/VCvtPkU8F32RoundMode.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtPkU8F32RoundMode.cpp index 777bd3e290..88725601dd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VCvtPkU8F32RoundMode.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtPkU8F32RoundMode.cpp @@ -26,6 +26,11 @@ alignas(256) constexpr std::array Code{ 0x80010a02u, 0xbf810000u, }; +alignas(256) std::array RoundModeCode{ + 0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xbfa40000u, 0xd55e000au, 0x02010104u, + 0xe0701000u, 0x80010a02u, 0xbf810000u, +}; + struct PackVector { std::uint32_t value; std::uint32_t denormalsFlushed[4]; @@ -84,7 +89,7 @@ std::string Hex(std::uint32_t value) { return text; } -void Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode floatMode) { +void Run(AgcDriver::VulkanDevice& device, std::span code, ShaderRecompiler::ShaderFloatMode floatMode) { Input.fill(0u); for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) Input[lane * Inputs] = Vectors[lane].value; Output.fill(0xdeadbeefu); @@ -93,7 +98,6 @@ void Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode floa const auto output = BufferDescriptor(Output.data(), static_cast(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}, 0, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ @@ -109,11 +113,11 @@ void Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode floa device.WaitIdle(); } -void Check(bool kept, std::uint32_t rounding, bool ieee) { +void Check(bool kept, std::uint32_t rounding, bool ieee, const std::string& setting) { for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) { const auto& vector = Vectors[lane]; const auto expected = kept ? vector.denormalsKept[rounding] : vector.denormalsFlushed[rounding]; - Require(Output[lane] == expected, std::string("v_cvt_pk_u8_f32 round mode ") + std::to_string(rounding) + (kept ? ", f32 denormals kept" : ", f32 denormals flushed") + + Require(Output[lane] == expected, std::string("v_cvt_pk_u8_f32 round mode ") + std::to_string(rounding) + setting + (kept ? ", f32 denormals kept" : ", f32 denormals flushed") + (ieee ? ", IEEE=1" : ", IEEE=0") + " of " + Hex(vector.value) + " is " + Hex(Output[lane]) + ", expected " + Hex(expected)); } } @@ -124,11 +128,16 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - for (const bool kept : {false, true}) { + for (std::uint32_t denormals = 0; denormals < 4u; ++denormals) { + const bool kept = (denormals & 1u) != 0u; + const std::string field = ", f32 denormal mode " + std::to_string(denormals); for (std::uint32_t rounding = 0; rounding < 4u; ++rounding) { for (const bool ieee : {false, true}) { - Run(*device, ShaderRecompiler::ShaderFloatMode{(kept ? 0xf0u : 0xc0u) | rounding, true, ieee, false}); - Check(kept, rounding, ieee); + Run(*device, Code, ShaderRecompiler::ShaderFloatMode{0xc0u | denormals << 4u | rounding, true, ieee, false}); + Check(kept, rounding, ieee, field); + RoundModeCode[5] = 0xbfa40000u | rounding; + Run(*device, RoundModeCode, ShaderRecompiler::ShaderFloatMode{0xc0u | denormals << 4u, true, ieee, false}); + Check(kept, rounding, ieee, field + " set by s_round_mode"); } } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp index 08adb9fde4..23c82c25eb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp @@ -18,6 +18,7 @@ using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Width = 192; constexpr std::uint32_t Height = 128; constexpr std::uint32_t DistanceVector = 0x00400000u; +constexpr std::uint32_t CullDistanceEnables = 0x0000ff00u; alignas(256) std::array Pixels{}; alignas(256) constexpr std::array VertexCode{ @@ -185,14 +186,19 @@ int main() { std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); return VulkanTestSkipped; } + const bool cullDistance = TargetHasCapability(device->Target(), spv::CapabilityCullDistance); + if (!cullDistance) std::puts("cull distance cases skipped, the device has no shaderCullDistance"); + const auto runs = [&](const Row& row) { return cullDistance || (row.control & CullDistanceEnables) == 0u; }; for (const auto waveSize : {64u, 32u}) { for (const auto& row : Rows) { + if (!runs(row)) continue; Draw(*device, waveSize, row, false); Check(row, waveSize, false); } } if (device->Target().mesh.has_value()) { for (const auto& row : Rows) { + if (!runs(row)) continue; Draw(*device, 64u, row, true); Check(row, 64u, true); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDraw.cpp index 076e006c49..b4d62cdc54 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDraw.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDraw.cpp @@ -33,6 +33,7 @@ using AgcDriver::Graphics::Require; constexpr std::uint32_t Width = 192; constexpr std::uint32_t Height = 128; constexpr std::uint32_t DistanceVector = 0x00400000u; +constexpr std::uint32_t CullDistanceEnables = 0x0000ff00u; constexpr std::uint32_t SetContextReg = 0x69u; constexpr std::uint32_t SetShReg = 0x76u; constexpr std::uint32_t SetUconfigReg = 0x79u; @@ -212,14 +213,18 @@ int main(int argc, char** argv) { const bool noKey = argc > 1 && std::string(argv[1]) == "no-key"; if (noKey) Require(std::getenv("APS5_NO_DRAW_KEY") != nullptr, "the no-key run must start with APS5_NO_DRAW_KEY set"); PositionExportLayoutTests(); + bool cullDistance = false; { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + cullDistance = TargetHasCapability(device->Target(), spv::CapabilityCullDistance); } + if (!cullDistance) std::puts("cull distance steps skipped, the device has no shaderCullDistance"); ColorMemory = AllocateWatched(2 * Block); Register(VertexShader, VertexCode.data(), sizeof(VertexCode), 2); Register(PixelShader, PixelCode.data(), sizeof(PixelCode), 1); for (const auto& step : Steps) { + if (!cullDistance && (step.control & CullDistanceEnables) != 0u) continue; std::array vertices{}; for (std::size_t i = 0; i < vertices.size(); ++i) { vertices[i] = {{PositionX[i], PositionY[i], 0.5f, 1.0f}, {step.component0[i], step.component1[i], step.component2[i], 0.0f}}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp index df9e6f2c6d..2130d6f232 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp @@ -125,7 +125,7 @@ void ControlStateDraws(std::vector prefix) { CheckBranchColors(activeExec, emptyExec, "EXEC"); } -void VertexTailDraws() { +void VertexTailDraws(bool cullDistance) { std::vector prefix{ 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, 0x7e060280u, 0x7e0802f2u, 0xbe8c03ffu, 0x3f800000u, 0xbf060c0cu, @@ -142,8 +142,12 @@ void VertexTailDraws() { alignas(256) const std::array clipContinuation{0xbf840005u, 0x7e08020cu, 0xf80000cfu, 0x04030201u, 0xf80008dfu, 0x01010101u, 0xbf810000u}; const auto clipped = Check(prefix, clipContinuation, 0x00400001u, clipContinuation.size()); Require(clipped != original && std::count(clipped.begin(), clipped.end(), Kept) > kept, "a clip distance exported by the continuation did not clip the triangle"); - const auto culled = Check(prefix, clipContinuation, 0x00400100u, clipContinuation.size()); - Require(culled == original, "a mixed-sign cull distance discarded the triangle"); + if (cullDistance) { + const auto culled = Check(prefix, clipContinuation, 0x00400100u, clipContinuation.size()); + Require(culled == original, "a mixed-sign cull distance discarded the triangle"); + } else { + std::puts("cull distance draw skipped, the device has no shaderCullDistance"); + } ControlStateDraws(prefix); } @@ -151,11 +155,13 @@ void VertexTailDraws() { int main() { try { + bool cullDistance = false; { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + cullDistance = TargetHasCapability(device->Target(), spv::CapabilityCullDistance); } - VertexTailDraws(); + VertexTailDraws(cullDistance); AgcDriverShutdown_nid_postfix(); std::puts("vertex user-data tail tests passed"); return 0; diff --git a/core/libs/prx/libSceAjm.native/CMakeLists.txt b/core/libs/prx/libSceAjm.native/CMakeLists.txt index 20f6cf1f20..9f0a86e55b 100644 --- a/core/libs/prx/libSceAjm.native/CMakeLists.txt +++ b/core/libs/prx/libSceAjm.native/CMakeLists.txt @@ -2,7 +2,7 @@ enable_language(C) set(LIBATRAC9_DIR ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) set(LIBATRAC9_PATCHED_DIR ${CMAKE_CURRENT_BINARY_DIR}/atrac9) -set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${LIBATRAC9_DIR}/unpack.c ${LIBATRAC9_DIR}/bit_reader.c ${LIBATRAC9_DIR}/decinit.c) +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${LIBATRAC9_DIR}/unpack.c ${LIBATRAC9_DIR}/bit_reader.c ${LIBATRAC9_DIR}/decinit.c ${LIBATRAC9_DIR}/utility.c) include(${CMAKE_CURRENT_SOURCE_DIR}/PatchLibAtrac9.cmake) add_library(atrac9 STATIC EXCLUDE_FROM_ALL ${LIBATRAC9_DIR}/band_extension.c @@ -17,7 +17,7 @@ add_library(atrac9 STATIC EXCLUDE_FROM_ALL ${LIBATRAC9_DIR}/scale_factors.c ${LIBATRAC9_DIR}/tables.c ${LIBATRAC9_PATCHED_DIR}/unpack.c - ${LIBATRAC9_DIR}/utility.c + ${LIBATRAC9_PATCHED_DIR}/utility.c ) target_include_directories(atrac9 PRIVATE ${LIBATRAC9_DIR}) set_target_properties(atrac9 PROPERTIES POSITION_INDEPENDENT_CODE ON) @@ -36,6 +36,9 @@ if(BUILD_TESTING) add_test_executable(atrac9_gradient_tests tests/Atrac9Gradient.c ${LIBATRAC9_DIR}/bit_reader.c) target_include_directories(atrac9_gradient_tests PRIVATE ${LIBATRAC9_PATCHED_DIR} ${LIBATRAC9_DIR}) target_link_libraries(atrac9_gradient_tests PRIVATE atrac9) + if(NOT WIN32) + target_link_libraries(atrac9_gradient_tests PRIVATE m) + endif() target_compile_options(atrac9_gradient_tests PRIVATE -ffunction-sections -fdata-sections) if(APPLE) target_link_options(atrac9_gradient_tests PRIVATE LINKER:-dead_strip) diff --git a/core/libs/prx/libSceAjm.native/PatchLibAtrac9.cmake b/core/libs/prx/libSceAjm.native/PatchLibAtrac9.cmake index 6c1b91c006..ed0a64c6e9 100644 --- a/core/libs/prx/libSceAjm.native/PatchLibAtrac9.cmake +++ b/core/libs/prx/libSceAjm.native/PatchLibAtrac9.cmake @@ -28,3 +28,21 @@ if(libatrac9ConfigGuardPosition EQUAL -1) endif() string(REPLACE "${libatrac9OldConfigGuard}" "${libatrac9NewConfigGuard}" libatrac9Decinit "${libatrac9Decinit}") file(WRITE ${LIBATRAC9_PATCHED_DIR}/decinit.c "${libatrac9Decinit}") + +file(READ ${LIBATRAC9_DIR}/utility.c libatrac9Utility) +set(libatrac9OldReverse "\treturn value >> (32 - bitCount);") +set(libatrac9NewReverse "\treturn bitCount ? value >> (32 - bitCount) : 0u;") +string(FIND "${libatrac9Utility}" "${libatrac9OldReverse}" libatrac9ReversePosition) +if(libatrac9ReversePosition EQUAL -1) + message(FATAL_ERROR "LibAtrac9 BitReverse32 has changed; review the local shift-by-32 guard") +endif() +string(REPLACE "${libatrac9OldReverse}" "${libatrac9NewReverse}" libatrac9Utility "${libatrac9Utility}") + +set(libatrac9OldSignExtend "\treturn (value << shift) >> shift;") +set(libatrac9NewSignExtend "\treturn (int)((unsigned int)value << shift) >> shift;") +string(FIND "${libatrac9Utility}" "${libatrac9OldSignExtend}" libatrac9SignExtendPosition) +if(libatrac9SignExtendPosition EQUAL -1) + message(FATAL_ERROR "LibAtrac9 SignExtend32 has changed; review the local signed-shift guard") +endif() +string(REPLACE "${libatrac9OldSignExtend}" "${libatrac9NewSignExtend}" libatrac9Utility "${libatrac9Utility}") +file(WRITE ${LIBATRAC9_PATCHED_DIR}/utility.c "${libatrac9Utility}") diff --git a/core/libs/prx/libSceAjm.native/include/Api.hpp b/core/libs/prx/libSceAjm.native/include/Api.hpp index 3c1f8608ed..6547522846 100644 --- a/core/libs/prx/libSceAjm.native/include/Api.hpp +++ b/core/libs/prx/libSceAjm.native/include/Api.hpp @@ -70,5 +70,9 @@ int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int p int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout, AjmBatchError* error); int APS5_VABI sceAjmBatchCancel(uint32_t context, uint32_t batch); int APS5_VABI sceAjmBatchErrorDump(const AjmBatchInfo* info, AjmBatchError* error); +void* APS5_VABI sceAjmBatchJobControlBufferRa(void* position, uint32_t instance, uint64_t flags, const void* sideband_input, size_t sideband_input_size, void* sideband_output, size_t sideband_output_size, void* return_address); +void* APS5_VABI sceAjmBatchJobRunBufferRa(void* position, uint32_t instance, uint64_t flags, const void* data_input, size_t data_input_size, void* data_output, size_t data_output_size, void* sideband_output, size_t sideband_output_size, void* return_address); +void* APS5_VABI sceAjmBatchJobRunSplitBufferRa(void* position, uint32_t instance, uint64_t flags, const AjmBuffer* input_buffers, size_t input_buffers_num, const AjmBuffer* output_buffers, size_t output_buffers_num, void* sideband_output, size_t sideband_output_size, void* return_address); +int APS5_VABI sceAjmBatchStartBuffer(uint32_t context, const uint8_t* batch, uint32_t batch_size, int priority, AjmBatchError* error, uint32_t* batch_id); } diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 7661f2477d..72284a8281 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -36,6 +36,8 @@ constexpr int SCE_AJM_ERROR_INVALID_INSTANCE = static_cast(0x80930003); constexpr int SCE_AJM_ERROR_INVALID_BATCH = static_cast(0x80930004); constexpr int SCE_AJM_ERROR_INVALID_PARAMETER = static_cast(0x80930005); constexpr int SCE_AJM_ERROR_OUT_OF_RESOURCES = static_cast(0x80930007); +constexpr int SCE_AJM_ERROR_MALFORMED_BATCH = static_cast(0x80930011); +constexpr int SCE_AJM_ERROR_INVALID_ADDRESS = static_cast(0x80930016); constexpr std::int32_t AJM_RESULT_NOT_INITIALIZED = 0x00000001; constexpr std::int32_t AJM_RESULT_INVALID_DATA = 0x00000002; @@ -216,6 +218,7 @@ struct JobHeader { std::uint32_t inputCount; std::uint32_t outputCount; std::uint8_t parameters[16]; + bool console; }; struct PackedJob { @@ -691,9 +694,17 @@ void EmitFrame(Instance& instance, PcmOutputs& outputs, const std::uint8_t* pcm, ResamplerProduce(instance, outputs, channels, encoding, sampleBytes); } +// A PS4 reports an ATRAC9 stream's bitrate from its superframe: 448-byte superframes of 4 frames of 256 +// samples at 48 kHz gave 168000 (Knowledge is Power: Decades, every run job's format sideband). +std::uint32_t At9Bitrate(const Instance& instance) { + const auto samples = static_cast(instance.info.framesInSuperframe) * static_cast(instance.info.frameSamples); + if (samples == 0) return 0; + return static_cast(static_cast(instance.info.superframeSize) * 8u * static_cast(instance.info.samplingRate) / samples); +} + SidebandFormat CurrentFormat(const Instance& instance) { switch (instance.codec) { - case CODEC_AT9: return {static_cast(instance.info.channels), ChannelMask(static_cast(instance.info.channels)), static_cast(instance.info.samplingRate), PcmEncoding(instance), 0, 0}; + case CODEC_AT9: return {static_cast(instance.info.channels), ChannelMask(static_cast(instance.info.channels)), static_cast(instance.info.samplingRate), PcmEncoding(instance), At9Bitrate(instance), 0}; case CODEC_MP3: return {instance.mp3Channels, ChannelMask(instance.mp3Channels), instance.mp3SampleRate, PcmEncoding(instance), instance.mp3Bitrate, 0}; case CODEC_OPUS: return {instance.opusChannels, ChannelMask(instance.opusChannels), instance.opusSampleRate, PcmEncoding(instance), 0, 0}; case CODEC_M4AAC: return {instance.aacChannels, ChannelMask(instance.aacChannels), instance.aacSampleRate, PcmEncoding(instance), instance.aacBitrate, 0}; @@ -768,12 +779,22 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c if (segmentEnded()) break; if (instance.superframeRemaining == 0) consumed += RiffDataOffset(input.data() + consumed, input.size() - consumed); const std::size_t needed = instance.superframeRemaining == 0 ? superframeSize : instance.superframeRemaining; + const bool roomForFrame = Resampling(instance) ? pcmOutputs.Room() >= channels * sampleBytes : pcmOutputs.Room() >= frameBytes; + // A PS4 reports which side ran out (probe runs of a title's ATRAC9 stream through libSceAjm's + // builders): PARTIAL_INPUT when the input ran out, on a superframe boundary or not, with room + // left; NOT_ENOUGH_ROOM when the room ran out with input left, after however many frames (and + // with none decoded for a zero room); both (0x18) when the input ended just as the output + // filled, which is every Wwise run job. A run without input (a format query) reports 0. + // Resampled runs were not measured and keep the earlier rule: a trailing partial superframe is + // partial input, a zero-output run is out of room. + const bool measured = !Resampling(instance); if (input.size() - consumed < needed) { - if (input.size() != consumed) result |= AJM_RESULT_PARTIAL_INPUT; + if (input.size() != consumed || (measured && !input.empty())) result |= AJM_RESULT_PARTIAL_INPUT; + if (measured && !input.empty() && !roomForFrame) result |= AJM_RESULT_NOT_ENOUGH_ROOM; break; } - if (Resampling(instance) ? pcmOutputs.Room() < channels * sampleBytes : pcmOutputs.Room() < frameBytes) { - if (frames == 0 && pcmOutputs.produced == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; + if (!roomForFrame) { + if (measured || (frames == 0 && pcmOutputs.produced == 0)) result |= AJM_RESULT_NOT_ENOUGH_ROOM; break; } if (instance.superframeRemaining == 0) { @@ -797,6 +818,7 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c ++frames; EmitFrame(instance, pcmOutputs, pcm.data(), frameSamples, channels, sampleBytes); + if (job.console && (job.flags & RUN_MULTIPLE_FRAMES) == 0) break; } if (segmentEnded()) instance.segmentSamples = 0; @@ -1692,6 +1714,169 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out } } +int RegisterBatch(std::uint32_t context, AjmBatchError* error, std::uint32_t* batch) { + if (error) std::memset(error, 0, sizeof(*error)); + const auto id = g_nextBatch.fetch_add(1, std::memory_order_relaxed); + { + std::lock_guard lock(g_batchLock); + g_batches.emplace(id, context); + } + *batch = id; + return 0; +} + +// The PS4 batch buffer (sceAjmBatchStartBuffer) is not private: the PS4 SDK builds it with inline code +// in the title, so it has the console's layout, taken from shadPS4's reimplementation of the PS4 +// library. It is a run of chunks, each starting with a 32-bit word that holds the chunk kind in bits +// 0-5 and a payload in bits 6-25. A job chunk has the instance as payload and, in a second word, the +// byte count of the chunks after it that describe the job; an inline chunk holds that many bytes of data +// a job's control buffer may point to. A job's flags chunk has flags bits 32-51 as payload and bits 0-31 +// in its second word; its buffer chunks have a 32-bit size and a 64-bit address. +enum class ConsoleChunk : std::uint32_t { + Job = 0, + InputRunBuffer = 1, + InputControlBuffer = 2, + ControlFlags = 3, + RunFlags = 4, + ReturnAddress = 6, + InlineBuffer = 7, + OutputRunBuffer = 17, + OutputControlBuffer = 18, +}; + +constexpr std::uint32_t CONSOLE_STATISTICS_INSTANCE = 0x80000; +constexpr std::uint64_t CONSOLE_FLAGS_VERSION_AND_CODEC = 0x7FF; + +// The low header word as the PS4's libSceAjm builders write it (a trace of 32 545 batches a Wwise title +// started on a PS4): bits 0-3 the kind, bit 4 the output direction of a buffer chunk +// (0x11 output run, 0x12 output control), bits 6-25 the job's instance or the flags' bits 32-51. The +// builders leave bits 5, 26, 27 and 31 as they were, and a title that reuses its batch memory (Wwise +// does) carries the previous batch's bytes there: a 6-bit kind read job headers as 16, 32 or 48 and run +// flags as 20, 36 or 52, and a 26-bit payload read flags bit 52 set. +ConsoleChunk ConsoleChunkKind(std::uint32_t word) { + const std::uint32_t kind = word & 0xFu; + return static_cast(kind == 1 || kind == 2 ? kind | (word & 0x10u) : kind); +} + +std::uint32_t ConsoleChunkPayload(std::uint32_t word) { + return (word >> 6) & 0xFFFFFu; +} + +std::uint8_t* PutConsoleChunk(std::uint8_t* at, ConsoleChunk kind, std::uint32_t payload, std::uint32_t word) { + const std::uint32_t words[2] = {static_cast(kind) | ((payload & 0xFFFFFu) << 6), word}; + std::memcpy(at, words, sizeof(words)); + return at + sizeof(words); +} + +std::uint8_t* PutConsoleFlags(std::uint8_t* at, ConsoleChunk kind, std::uint64_t flags) { + return PutConsoleChunk(at, kind, static_cast(flags >> 32), static_cast(flags)); +} + +std::uint8_t* PutConsoleBuffer(std::uint8_t* at, ConsoleChunk kind, const void* address, std::size_t size) { + at = PutConsoleChunk(at, kind, 0, static_cast(size)); + std::memcpy(at, &address, sizeof(address)); + return at + sizeof(address); +} + +std::uint8_t* BeginConsoleJob(std::uint8_t* job, const void* returnAddress) { + std::uint8_t* at = job + 2 * sizeof(std::uint32_t); + return returnAddress ? PutConsoleBuffer(at, ConsoleChunk::ReturnAddress, returnAddress, 0) : at; +} + +void* EndConsoleJob(std::uint8_t* job, std::uint8_t* end, std::uint32_t instance) { + PutConsoleChunk(job, ConsoleChunk::Job, instance, static_cast(end - job) - 2 * sizeof(std::uint32_t)); + return end; +} + +struct ConsoleJob { + JobHeader header; + std::vector buffers; +}; + +// What sceAjmBatchStartBuffer reports in the AjmBatchError when it refuses a batch: the error, the job +// chunk, the offending chunk's offset in the job and the job's return address, as a PS4 fills them for a +// buffer chunk with a null address and a nonzero size (INVALID_ADDRESS, nothing of the batch run). The +// other refusals here report MALFORMED_BATCH the same way; they were not measured. +struct ConsoleRefusal { + std::int32_t code = SCE_AJM_ERROR_MALFORMED_BATCH; + const std::uint8_t* job = nullptr; + std::uint32_t offset = 0; + const void* returnAddress = nullptr; +}; + +// Reads one console job: the chunks between the job header at `job` and `end`. A buffer chunk of size +// zero is dropped whatever its address, as a PS4 does. Returns false with the refusal filled for a null +// address with a nonzero size, for chunks that overrun the job and for a job without flags. +bool ReadConsoleJob(std::uint32_t instance, const std::uint8_t* job, const std::uint8_t* end, ConsoleJob& out, ConsoleRefusal& refusal) { + out.header = JobHeader{}; + out.header.instance = instance; + out.header.console = true; + std::vector inputs; + std::vector outputs; + AjmBuffer controlInput{}; + AjmBuffer controlOutput{}; + ConsoleChunk flagsKind = ConsoleChunk::Job; + refusal = ConsoleRefusal{}; + refusal.job = job; + const std::uint8_t* at = job + 2 * sizeof(std::uint32_t); + while (at < end) { + refusal.offset = static_cast(at - job); + if (end - at < 8) return false; + std::uint32_t words[2]; + std::memcpy(words, at, sizeof(words)); + const auto kind = ConsoleChunkKind(words[0]); + if (kind == ConsoleChunk::ControlFlags || kind == ConsoleChunk::RunFlags) { + if (flagsKind != ConsoleChunk::Job) return false; + flagsKind = kind; + out.header.flags = (std::uint64_t{ConsoleChunkPayload(words[0])} << 32) | words[1]; + at += sizeof(words); + continue; + } + if (kind != ConsoleChunk::InputRunBuffer && kind != ConsoleChunk::InputControlBuffer && kind != ConsoleChunk::ReturnAddress && kind != ConsoleChunk::OutputRunBuffer && kind != ConsoleChunk::OutputControlBuffer) { + NotImplemented_nid_no_patch(("sceAjmBatchStartBuffer (job chunk kind " + std::to_string(static_cast(kind)) + ")").c_str()); + } + if (end - at < 16) return false; + AjmBuffer buffer{nullptr, words[1]}; + std::memcpy(&buffer.ptr, at + sizeof(words), sizeof(buffer.ptr)); + at += 16; + if (kind == ConsoleChunk::ReturnAddress) { + refusal.returnAddress = buffer.ptr; + continue; + } + if (buffer.size == 0) continue; + if (!buffer.ptr) { + refusal.code = SCE_AJM_ERROR_INVALID_ADDRESS; + return false; + } + if (kind == ConsoleChunk::InputRunBuffer) inputs.push_back(buffer); + if (kind == ConsoleChunk::OutputRunBuffer) outputs.push_back(buffer); + if (kind == ConsoleChunk::InputControlBuffer) controlInput = buffer; + if (kind == ConsoleChunk::OutputControlBuffer) controlOutput = buffer; + } + refusal.offset = static_cast(end - job); + if (flagsKind == ConsoleChunk::Job) return false; + JobHeader& header = out.header; + header.sideband = controlOutput.ptr; + header.sidebandSize = controlOutput.size; + if (instance == CONSOLE_STATISTICS_INSTANCE) { + header.kind = JobKind::GetStatistics; + } else if (flagsKind == ConsoleChunk::ControlFlags) { + constexpr std::uint64_t implemented = CONSOLE_FLAGS_VERSION_AND_CODEC | CONTROL_RESET | CONTROL_INITIALIZE | SIDEBAND_GAPLESS_DECODE; + if ((header.flags & ~implemented) != 0) NotImplemented_nid_no_patch("sceAjmBatchStartBuffer (control job flags other than RESET, INITIALIZE and SIDEBAND_GAPLESS_DECODE)"); + if (!inputs.empty() || !outputs.empty()) NotImplemented_nid_no_patch("sceAjmBatchStartBuffer (control job with run buffers)"); + header.kind = JobKind::Control; + if (controlInput.size) inputs.push_back(controlInput); + } else { + if (controlInput.size) NotImplemented_nid_no_patch("sceAjmBatchStartBuffer (run job with an input control buffer)"); + header.kind = JobKind::Run; + } + header.inputCount = static_cast(inputs.size()); + header.outputCount = static_cast(outputs.size()); + AJM_TRACE("[ajm] console job: instance %u, %s flags 0x%llx, %u input and %u output buffers, control input %llu and output %llu bytes\n", instance, flagsKind == ConsoleChunk::ControlFlags ? "control" : "run", static_cast(header.flags), header.inputCount, header.outputCount, static_cast(controlInput.size), static_cast(controlOutput.size)); + out.buffers = std::move(inputs); + out.buffers.insert(out.buffers.end(), outputs.begin(), outputs.end()); + return true; +} } extern "C" { @@ -1893,14 +2078,19 @@ int APS5_VABI sceAjmBatchJobGetStatistics(AjmBatchInfo* info, float interval, vo int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int priority, AjmBatchError* error, uint32_t* batch) { if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT; (void)priority; - if (!info || !batch) return SCE_AJM_ERROR_INVALID_PARAMETER; + if (!info || !batch || !info->p_buffer || info->offset > info->size) return SCE_AJM_ERROR_INVALID_PARAMETER; AJM_TRACE("[ajm] batch start: context %u, priority %d, %llu of %llu buffer bytes used\n", context, priority, static_cast(info->offset), static_cast(info->size)); const auto* cursor = static_cast(info->p_buffer); - const auto* end = cursor + info->offset; + auto remaining = info->offset; std::vector buffers; - while (cursor < end) { + while (remaining != 0) { + if (remaining < sizeof(PackedJob)) NotImplemented_nid_no_patch("AJM batch contains a truncated private job header"); PackedJob record; std::memcpy(&record, cursor, sizeof(record)); + const std::size_t bufferCount = static_cast(record.inputCount) + record.outputCount; + const std::size_t parameterBytes = (static_cast(record.parameterSize) + 7u) & ~std::size_t{7}; + const std::size_t requiredBytes = sizeof(PackedJob) + bufferCount * sizeof(AjmBuffer) + parameterBytes; + if (record.parameterSize > sizeof(JobHeader::parameters) || record.bytes < requiredBytes || record.bytes > remaining) NotImplemented_nid_no_patch("AJM batch contains an invalid private job size"); JobHeader job{}; job.kind = static_cast(record.kind); job.bytes = record.bytes; @@ -1915,16 +2105,10 @@ int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int p if (!buffers.empty()) std::memcpy(buffers.data(), cursor + sizeof(record), buffers.size() * sizeof(AjmBuffer)); if (record.parameterSize) std::memcpy(job.parameters, cursor + sizeof(record) + buffers.size() * sizeof(AjmBuffer), record.parameterSize); Execute(job, buffers.data(), buffers.data() + job.inputCount); - cursor += record.bytes; + remaining -= record.bytes; + if (remaining != 0) cursor += record.bytes; } - if (error) std::memset(error, 0, sizeof(*error)); - const auto id = g_nextBatch.fetch_add(1, std::memory_order_relaxed); - { - std::lock_guard lock(g_batchLock); - g_batches.emplace(id, context); - } - *batch = id; - return 0; + return RegisterBatch(context, error, batch); } int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout, AjmBatchError* error) { @@ -1953,4 +2137,69 @@ int APS5_VABI sceAjmBatchErrorDump(const AjmBatchInfo* info, AjmBatchError* erro return 0; } +void* APS5_VABI sceAjmBatchJobControlBufferRa(void* position, uint32_t instance, uint64_t flags, const void* sideband_input, size_t sideband_input_size, void* sideband_output, size_t sideband_output_size, void* return_address) { + auto* job = static_cast(position); + auto* at = BeginConsoleJob(job, return_address); + at = PutConsoleBuffer(at, ConsoleChunk::InputControlBuffer, sideband_input, sideband_input_size); + at = PutConsoleFlags(at, ConsoleChunk::ControlFlags, flags); + at = PutConsoleBuffer(at, ConsoleChunk::OutputControlBuffer, sideband_output, sideband_output_size); + return EndConsoleJob(job, at, instance); +} + +void* APS5_VABI sceAjmBatchJobRunSplitBufferRa(void* position, uint32_t instance, uint64_t flags, const AjmBuffer* input_buffers, size_t input_buffers_num, const AjmBuffer* output_buffers, size_t output_buffers_num, void* sideband_output, size_t sideband_output_size, void* return_address) { + if ((input_buffers_num && !input_buffers) || (output_buffers_num && !output_buffers)) NotImplemented_nid_no_patch("AJM batch job with a null buffer list and a nonzero buffer count"); + auto* job = static_cast(position); + auto* at = BeginConsoleJob(job, return_address); + for (std::size_t index = 0; index < input_buffers_num; ++index) at = PutConsoleBuffer(at, ConsoleChunk::InputRunBuffer, input_buffers[index].ptr, input_buffers[index].size); + at = PutConsoleFlags(at, ConsoleChunk::RunFlags, flags); + for (std::size_t index = 0; index < output_buffers_num; ++index) at = PutConsoleBuffer(at, ConsoleChunk::OutputRunBuffer, output_buffers[index].ptr, output_buffers[index].size); + at = PutConsoleBuffer(at, ConsoleChunk::OutputControlBuffer, sideband_output, sideband_output_size); + return EndConsoleJob(job, at, instance); +} + +void* APS5_VABI sceAjmBatchJobRunBufferRa(void* position, uint32_t instance, uint64_t flags, const void* data_input, size_t data_input_size, void* data_output, size_t data_output_size, void* sideband_output, size_t sideband_output_size, void* return_address) { + const AjmBuffer input{const_cast(data_input), data_input_size}; + const AjmBuffer output{data_output, data_output_size}; + return sceAjmBatchJobRunSplitBufferRa(position, instance, flags, &input, 1, &output, 1, sideband_output, sideband_output_size, return_address); +} + +int APS5_VABI sceAjmBatchStartBuffer(uint32_t context, const uint8_t* batch, uint32_t batch_size, int priority, AjmBatchError* error, uint32_t* batch_id) { + if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT; + if (!batch_id || !batch) return SCE_AJM_ERROR_INVALID_PARAMETER; + AJM_TRACE("[ajm] batch start buffer: context %u, priority %d, %u bytes\n", context, priority, batch_size); + std::vector jobs; + ConsoleRefusal refusal; + const std::uint8_t* at = batch; + const std::uint8_t* end = batch + batch_size; + bool accepted = batch_size % 8 == 0; + while (accepted && at < end) { + refusal = ConsoleRefusal{}; + refusal.job = at; + if (end - at < 8) { + accepted = false; + break; + } + std::uint32_t words[2]; + std::memcpy(words, at, sizeof(words)); + const std::uint8_t* body = at + sizeof(words); + if (words[1] > static_cast(end - body)) { + accepted = false; + break; + } + const auto kind = ConsoleChunkKind(words[0]); + if (kind == ConsoleChunk::Job) { + accepted = ReadConsoleJob(ConsoleChunkPayload(words[0]), at, body + words[1], jobs.emplace_back(), refusal); + } else if (kind != ConsoleChunk::InlineBuffer) { + NotImplemented_nid_no_patch(("sceAjmBatchStartBuffer (batch chunk kind " + std::to_string(static_cast(kind)) + ")").c_str()); + } + at = body + words[1]; + } + if (!accepted) { + if (error) *error = AjmBatchError{refusal.code, refusal.job, refusal.offset, refusal.returnAddress}; + return SCE_AJM_ERROR_MALFORMED_BATCH; + } + for (const ConsoleJob& job : jobs) Execute(job.header, job.buffers.data(), job.buffers.data() + job.header.inputCount); + return RegisterBatch(context, error, batch_id); +} + } diff --git a/core/libs/prx/libSceAjm/Export.cpp b/core/libs/prx/libSceAjm/Export.cpp index be23cfdc39..87830816c5 100644 --- a/core/libs/prx/libSceAjm/Export.cpp +++ b/core/libs/prx/libSceAjm/Export.cpp @@ -6,19 +6,6 @@ extern "C" { -int APS5_VABI sceAjmBatchJobControlBufferRa() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAjmBatchJobRunSplitBufferRa() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAjmBatchStartBuffer() { - NotImplemented_nid_no_patch(__func__); - return 0; -} +APS5_DUMMY_FUN } diff --git a/core/libs/prx/libSceAjm/include/Api.hpp b/core/libs/prx/libSceAjm/include/Api.hpp index 1be3b9a23e..2cc01a795b 100644 --- a/core/libs/prx/libSceAjm/include/Api.hpp +++ b/core/libs/prx/libSceAjm/include/Api.hpp @@ -4,8 +4,4 @@ extern "C" { -int APS5_VABI sceAjmBatchJobControlBufferRa(); -int APS5_VABI sceAjmBatchJobRunSplitBufferRa(); -int APS5_VABI sceAjmBatchStartBuffer(); - } diff --git a/core/libs/prx/libSceAjmi/Export.cpp b/core/libs/prx/libSceAjmi/Export.cpp index 3941ee53aa..bdb3e1159d 100644 --- a/core/libs/prx/libSceAjmi/Export.cpp +++ b/core/libs/prx/libSceAjmi/Export.cpp @@ -13,8 +13,8 @@ int APS5_VABI sceAjmBatchJobInlineBuffer(void) { return 0; } -int APS5_VABI sceAjmBatchJobRunBufferRa(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAjmBatchJobRunBufferRa_nid_disambig1(void) { + NotImplemented_nid_no_patch("sceAjmBatchJobRunBufferRa"); return 0; } diff --git a/core/libs/prx/libSceAjmi/include/Api.hpp b/core/libs/prx/libSceAjmi/include/Api.hpp index cf36eb1119..f12efb729c 100644 --- a/core/libs/prx/libSceAjmi/include/Api.hpp +++ b/core/libs/prx/libSceAjmi/include/Api.hpp @@ -5,6 +5,6 @@ extern "C" { int APS5_VABI sceAjmBatchJobInlineBuffer(void); -int APS5_VABI sceAjmBatchJobRunBufferRa(void); +int APS5_VABI sceAjmBatchJobRunBufferRa_nid_disambig1(void); } diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index e5c956f2ee..3485cd7b86 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -288,9 +288,12 @@ int APS5_VABI sceAppContentGetDownloadedStoreCountry(void) { return 0; } -int APS5_VABI sceAppContentGetEntitlementKey(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAppContentGetEntitlementKey(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, AppContentEntitlementKey* key) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + (void)serviceLabel; + if (!entitlementLabel || !key) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlementLabel, 0x01500000)) return SCE_APP_CONTENT_ERROR_PARAMETER; + return SCE_APP_CONTENT_ERROR_DRM_NO_ENTITLEMENT; } int APS5_VABI sceAppContentGetPftFlag(void) { diff --git a/core/libs/prx/libSceAppContent/include/Api.hpp b/core/libs/prx/libSceAppContent/include/Api.hpp index 938424fd88..cd0a799e37 100644 --- a/core/libs/prx/libSceAppContent/include/Api.hpp +++ b/core/libs/prx/libSceAppContent/include/Api.hpp @@ -7,6 +7,10 @@ struct AppContentAddcontInfo { uint32_t status; }; +struct AppContentEntitlementKey { + char data[16]; +}; + extern "C" { int APS5_VABI sceAppContentAddcontMount(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, AppContentMountPoint* mountPoint); @@ -36,7 +40,7 @@ int APS5_VABI sceAppContentDownload1Shrink(void); int APS5_VABI sceAppContentGetAddcontInfoByEntitlementId(void); int APS5_VABI sceAppContentGetAddcontInfoListByIroTag(void); int APS5_VABI sceAppContentGetDownloadedStoreCountry(void); -int APS5_VABI sceAppContentGetEntitlementKey(void); +int APS5_VABI sceAppContentGetEntitlementKey(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, AppContentEntitlementKey* key); int APS5_VABI sceAppContentGetPftFlag(void); int APS5_VABI sceAppContentGetRegion(void); int APS5_VABI sceAppContentRequestPatchInstall(void); diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 4459dc8152..44cb922956 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -663,6 +663,10 @@ int APS5_VABI sceAudioOutSysOpen(int userId, std::uint32_t mode) { return allocatePort(PORT_TYPE_SYS, SYS_PORT_SAMPLES, SYS_PORT_FRAME_RATE, Format::S16_8Ch); } +int APS5_VABI sceAudioOutSetMainOutput() { + return 0; +} + int APS5_VABI sceAudioOutSetMixLevelPadSpk(int handle, std::uint32_t mixLevel) { std::lock_guard lock(g_mutex); Port* port = getPort(handle); diff --git a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp index 1e44d76aa6..55134a8ef6 100644 --- a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp +++ b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp @@ -129,11 +129,6 @@ int APS5_VABI sceAudioOutGetFocusEnablePid(void) { return 0; } -int APS5_VABI sceAudioOutSetMainOutput(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOutSetVolumeDown(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutSys.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutSys.cpp index 73ea859c0b..6c8f9561ee 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOutSys.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutSys.cpp @@ -46,6 +46,10 @@ int main() { Require(Throws([] { sceAudioOutSysConfigureOutput(1, 0, 4, 1, 0); }), "another mode throws"); Require(Throws([] { sceAudioOutSysConfigureOutput(1, 0, 5, 2, 0); }), "another target throws"); + int mainOutput = -1; + Require(!Throws([&mainOutput] { mainOutput = sceAudioOutSetMainOutput(); }), "sceAudioOutSetMainOutput does not throw"); + Require(mainOutput == 0, "sceAudioOutSetMainOutput returns 0"); + Require(sceAudioOutSysClose(truehd) == 0, "close the first Sys port"); Require(Throws([truehd] { sceAudioOutSysClose(truehd); }), "a closed Sys port throws"); const int ex = sceAudioOutExOpen(SYSTEM_USER, 0); diff --git a/core/libs/prx/libSceAudioPropagation/Export.cpp b/core/libs/prx/libSceAudioPropagation/Export.cpp index 2c257811bf..5d8dc47656 100644 --- a/core/libs/prx/libSceAudioPropagation/Export.cpp +++ b/core/libs/prx/libSceAudioPropagation/Export.cpp @@ -360,7 +360,8 @@ std::int32_t APS5_VABI sceAudioPropagationSourceCalculateAudioPaths(AudioPropaga if (found == state.objects.end() || found->second.kind != Kind::Source || !liveSystem(state, found->second.system)) return ErrorInvalidHandle; if (!rays || !paths) return ErrorNullPointer; if (rayCount != 6 || reflectionOrder == 0 || pathCount == 0) return ErrorInvalidArgument; - throw std::runtime_error(__func__); + NotImplemented_nid_no_patch(__func__); + return 0; } std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPaths(AudioPropagationHandle source, const void* entries, std::uint32_t count) { diff --git a/core/libs/prx/libSceAvPlayer/Export.cpp b/core/libs/prx/libSceAvPlayer/Export.cpp index a580ff2899..be3c03f01e 100644 --- a/core/libs/prx/libSceAvPlayer/Export.cpp +++ b/core/libs/prx/libSceAvPlayer/Export.cpp @@ -152,7 +152,7 @@ int APS5_VABI sceAvPlayerSetAvailableBandwidth(AvPlayerInternal* h, uint32_t sta (void)minimum_bandwidth; (void)maximum_bandwidth; if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return SCE_OK; + return ToPlayer(h)->SetAvailableBandwidth(); } int APS5_VABI sceAvPlayerSetLogCallback(void* callback, void* user_data) { diff --git a/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp b/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp index 4e7f7f7759..d81882a932 100644 --- a/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp +++ b/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp @@ -127,6 +127,7 @@ public: int JumpToTime(std::uint64_t milliseconds); int SetLooping(bool enabled); int SetTrickSpeed(std::int32_t trickSpeed); + int SetAvailableBandwidth(); int SetAvSyncMode(std::uint32_t mode); bool GetVideoData(AvPlayerFrameInfoEx& info); bool GetVideoData(AvPlayerFrameInfo& info); diff --git a/core/libs/prx/libSceAvPlayer/src/Player.cpp b/core/libs/prx/libSceAvPlayer/src/Player.cpp index f253ee42f4..3f928b7ea8 100644 --- a/core/libs/prx/libSceAvPlayer/src/Player.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Player.cpp @@ -334,14 +334,20 @@ int Player::SetLooping(bool enabled) { } int Player::SetTrickSpeed(std::int32_t trickSpeed) { - if (trickSpeed == 0) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; std::lock_guard lock(mutex); + if (!source) return SCE_AVPLAYER_ERROR_OPERATION_FAILED; + if (trickSpeed == 0) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; if (state == State::Stop || state == State::EndOfFile || state == State::Error) return SCE_AVPLAYER_ERROR_OPERATION_FAILED; speed = trickSpeed; - if (source) source->SetSpeed(trickSpeed); + source->SetSpeed(trickSpeed); return SCE_OK; } +int Player::SetAvailableBandwidth() { + std::lock_guard lock(mutex); + return source ? SCE_AVPLAYER_ERROR_OPERATION_FAILED : SCE_OK; +} + int Player::SetAvSyncMode(std::uint32_t mode) { if (mode != SyncModeDefault && mode != SyncModeNone) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; std::lock_guard lock(mutex); diff --git a/core/libs/prx/libSceBgft/Export.cpp b/core/libs/prx/libSceBgft/Export.cpp index 2d5833b1d3..c94cfeb6b8 100644 --- a/core/libs/prx/libSceBgft/Export.cpp +++ b/core/libs/prx/libSceBgft/Export.cpp @@ -85,7 +85,7 @@ int APS5_VABI sceBgftServiceIntDownloadGetGameAndGameAcProgress(const char* cont NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBgftServiceIntDownloadGetPatchProgress() { +int APS5_VABI sceBgftServiceIntDownloadGetPatchProgress(const char* contentId, BgftProgress* progress) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -97,7 +97,7 @@ int APS5_VABI sceBgftServiceIntDownloadGetProgress(int32_t taskId, BgftProgress* NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBgftServiceIntDownloadGetTaskInfo() { +int APS5_VABI sceBgftServiceIntDownloadGetTaskInfo(int32_t taskId, BgftTaskInfo* taskInfo) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceBgft/include/Api.hpp b/core/libs/prx/libSceBgft/include/Api.hpp index 484cf37682..9e39227e29 100644 --- a/core/libs/prx/libSceBgft/include/Api.hpp +++ b/core/libs/prx/libSceBgft/include/Api.hpp @@ -8,6 +8,7 @@ struct BgftInitParam { }; struct BgftProgress; +struct BgftTaskInfo; struct BgftStorageTaskParam { std::uint32_t value0; @@ -47,10 +48,10 @@ int APS5_VABI sceBgftServiceDownloadStopTask(int32_t taskId); int APS5_VABI sceBgftServiceIntDownloadFindActiveGameAndGameAcTask(const char* contentId, int32_t* taskId); int APS5_VABI sceBgftServiceIntDownloadFindActiveTask(const char* contentId, int32_t subType, int32_t* taskId); int APS5_VABI sceBgftServiceIntDownloadGetGameAndGameAcProgress(const char* contentId, BgftProgress* progress); -int APS5_VABI sceBgftServiceIntDownloadGetPatchProgress(); +int APS5_VABI sceBgftServiceIntDownloadGetPatchProgress(const char* contentId, BgftProgress* progress); int APS5_VABI sceBgftServiceIntDownloadGetPlayGoProgress(const char* contentId, BgftProgress* progress); int APS5_VABI sceBgftServiceIntDownloadGetProgress(int32_t taskId, BgftProgress* progress); -int APS5_VABI sceBgftServiceIntDownloadGetTaskInfo(); +int APS5_VABI sceBgftServiceIntDownloadGetTaskInfo(int32_t taskId, BgftTaskInfo* taskInfo); int APS5_VABI sceBgftServiceIntDownloadPauseTask(int32_t taskId); int APS5_VABI sceBgftServiceIntDownloadRegisterTaskByStorageEx(const BgftStorageTaskParam* params, int32_t* taskId); int APS5_VABI sceBgftServiceIntDownloadResumeTask(int32_t taskId); diff --git a/core/libs/prx/libSceCesCs/Cp949.hpp b/core/libs/prx/libSceCesCs/Cp949.hpp index ec0166ce13..54c8333dd4 100644 --- a/core/libs/prx/libSceCesCs/Cp949.hpp +++ b/core/libs/prx/libSceCesCs/Cp949.hpp @@ -38,7 +38,7 @@ inline constexpr uint16_t TO_UCS2[126 * 190] = { 0xC6A8, 0xC6AA, 0xC6AB, 0xC6AC, 0xC6AD, 0xC6AE, 0xC6AF, 0xC6B2, 0xC6B3, 0xC6B5, 0xC6B6, 0xC6B7, 0xC6BB, 0xC6BC, 0xC6BD, 0xC6BE, 0xC6BF, 0xC6C2, 0xC6C4, 0xC6C6, 0xC6C7, 0xC6C8, 0xC6C9, 0xC6CA, 0xC6CB, 0xC6CE, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC6CF, 0xC6D1, 0xC6D2, 0xC6D3, 0xC6D5, 0xC6D6, 0xC6D7, 0xC6D8, 0xC6D9, 0xC6DA, 0xC6DB, 0xC6DE, 0xC6DF, 0xC6E2, 0xC6E3, 0xC6E4, 0xC6E5, 0xC6E6, 0xC6E7, 0xC6EA, 0xC6EB, 0xC6ED, 0xC6EE, 0xC6EF, 0xC6F1, 0xC6F2, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC6F3, 0xC6F4, 0xC6F5, 0xC6F6, 0xC6F7, 0xC6FA, 0xC6FB, 0xC6FC, 0xC6FE, 0xC6FF, 0xC700, 0xC701, 0xC702, 0xC703, 0xC706, 0xC707, 0xC709, 0xC70A, 0xC70B, 0xC70D, 0xC70E, 0xC70F, 0xC710, 0xC711, 0xC712, 0xC713, 0xC716, 0xC718, 0xC71A, 0xC71B, 0xC71C, 0xC71D, 0xC71E, 0xC71F, 0xC722, 0xC723, 0xC725, 0xC726, 0xC727, 0xC729, 0xC72A, 0xC72B, 0xC72C, 0xC72D, 0xC72E, 0xC72F, 0xC732, 0xC734, 0xC736, 0xC738, 0xC739, 0xC73A, 0xC73B, 0xC73E, 0xC73F, 0xC741, 0xC742, 0xC743, 0xC745, 0xC746, 0xC747, 0xC748, 0xC749, 0xC74B, 0xC74E, 0xC750, 0xC759, 0xC75A, 0xC75B, 0xC75D, 0xC75E, 0xC75F, 0xC761, 0xC762, 0xC763, 0xC764, 0xC765, 0xC766, 0xC767, 0xC769, 0xC76A, 0xC76C, 0xC76D, 0xC76E, 0xC76F, 0xC770, 0xC771, 0xC772, 0xC773, 0xC776, 0xC777, 0xC779, 0xC77A, 0xC77B, 0xC77F, 0xC780, 0xC781, 0xC782, 0xC786, 0xC78B, 0xC78C, 0xC78D, 0xC78F, 0xC792, 0xC793, 0xC795, 0xC799, 0xC79B, 0xC79C, 0xC79D, 0xC79E, 0xC79F, 0xC7A2, 0xC7A7, 0xC7A8, 0xC7A9, 0xC7AA, 0xC7AB, 0xC7AE, 0xC7AF, 0xC7B1, 0xC7B2, 0xC7B3, 0xC7B5, 0xC7B6, 0xC7B7, 0xC7B8, 0xC7B9, 0xC7BA, 0xC7BB, 0xC7BE, 0xC7C2, 0xC7C3, 0xC7C4, 0xC7C5, 0xC7C6, 0xC7C7, 0xC7CA, 0xC7CB, 0xC7CD, 0xC7CF, 0xC7D1, 0xC7D2, 0xC7D3, 0xC7D4, 0xC7D5, 0xC7D6, 0xC7D7, 0xC7D9, 0xC7DA, 0xC7DB, 0xC7DC, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC7DE, 0xC7DF, 0xC7E0, 0xC7E1, 0xC7E2, 0xC7E3, 0xC7E5, 0xC7E6, 0xC7E7, 0xC7E9, 0xC7EA, 0xC7EB, 0xC7ED, 0xC7EE, 0xC7EF, 0xC7F0, 0xC7F1, 0xC7F2, 0xC7F3, 0xC7F4, 0xC7F5, 0xC7F6, 0xC7F7, 0xC7F8, 0xC7F9, 0xC7FA, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC7FB, 0xC7FC, 0xC7FD, 0xC7FE, 0xC7FF, 0xC802, 0xC803, 0xC805, 0xC806, 0xC807, 0xC809, 0xC80B, 0xC80C, 0xC80D, 0xC80E, 0xC80F, 0xC812, 0xC814, 0xC817, 0xC818, 0xC819, 0xC81A, 0xC81B, 0xC81E, 0xC81F, 0xC821, 0xC822, 0xC823, 0xC825, 0xC826, 0xC827, 0xC828, 0xC829, 0xC82A, 0xC82B, 0xC82E, 0xC830, 0xC832, 0xC833, 0xC834, 0xC835, 0xC836, 0xC837, 0xC839, 0xC83A, 0xC83B, 0xC83D, 0xC83E, 0xC83F, 0xC841, 0xC842, 0xC843, 0xC844, 0xC845, 0xC846, 0xC847, 0xC84A, 0xC84B, 0xC84E, 0xC84F, 0xC850, 0xC851, 0xC852, 0xC853, 0xC855, 0xC856, 0xC857, 0xC858, 0xC859, 0xC85A, 0xC85B, 0xC85C, 0xC85D, 0xC85E, 0xC85F, 0xC860, 0xC861, 0xC862, 0xC863, 0xC864, 0xC865, 0xC866, 0xC867, 0xC868, 0xC869, 0xC86A, 0xC86B, 0xC86C, 0xC86D, 0xC86E, 0xC86F, 0xC872, 0xC873, 0xC875, 0xC876, 0xC877, 0xC879, 0xC87B, 0xC87C, 0xC87D, 0xC87E, 0xC87F, 0xC882, 0xC884, 0xC888, 0xC889, 0xC88A, 0xC88E, 0xC88F, 0xC890, 0xC891, 0xC892, 0xC893, 0xC895, 0xC896, 0xC897, 0xC898, 0xC899, 0xC89A, 0xC89B, 0xC89C, 0xC89E, 0xC8A0, 0xC8A2, 0xC8A3, 0xC8A4, 0xC8A5, 0xC8A6, 0xC8A7, 0xC8A9, 0xC8AA, 0xC8AB, 0xC8AC, 0xC8AD, 0xC8AE, 0xC8AF, 0xC8B0, 0xC8B1, 0xC8B2, 0xC8B3, 0xC8B4, 0xC8B5, 0xC8B6, 0xC8B7, 0xC8B8, 0xC8B9, 0xC8BA, 0xC8BB, 0xC8BE, 0xC8BF, 0xC8C0, 0xC8C1, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC8C2, 0xC8C3, 0xC8C5, 0xC8C6, 0xC8C7, 0xC8C9, 0xC8CA, 0xC8CB, 0xC8CD, 0xC8CE, 0xC8CF, 0xC8D0, 0xC8D1, 0xC8D2, 0xC8D3, 0xC8D6, 0xC8D8, 0xC8DA, 0xC8DB, 0xC8DC, 0xC8DD, 0xC8DE, 0xC8DF, 0xC8E2, 0xC8E3, 0xC8E5, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC8E6, 0xC8E7, 0xC8E8, 0xC8E9, 0xC8EA, 0xC8EB, 0xC8EC, 0xC8ED, 0xC8EE, 0xC8EF, 0xC8F0, 0xC8F1, 0xC8F2, 0xC8F3, 0xC8F4, 0xC8F6, 0xC8F7, 0xC8F8, 0xC8F9, 0xC8FA, 0xC8FB, 0xC8FE, 0xC8FF, 0xC901, 0xC902, 0xC903, 0xC907, 0xC908, 0xC909, 0xC90A, 0xC90B, 0xC90E, 0x3000, 0x3001, 0x3002, 0x00B7, 0x2025, 0x2026, 0x00A8, 0x3003, 0x00AD, 0x2015, 0x2225, 0xFF3C, 0x223C, 0x2018, 0x2019, 0x201C, 0x201D, 0x3014, 0x3015, 0x3008, 0x3009, 0x300A, 0x300B, 0x300C, 0x300D, 0x300E, 0x300F, 0x3010, 0x3011, 0x00B1, 0x00D7, 0x00F7, 0x2260, 0x2264, 0x2265, 0x221E, 0x2234, 0x00B0, 0x2032, 0x2033, 0x2103, 0x212B, 0xFFE0, 0xFFE1, 0xFFE5, 0x2642, 0x2640, 0x2220, 0x22A5, 0x2312, 0x2202, 0x2207, 0x2261, 0x2252, 0x00A7, 0x203B, 0x2606, 0x2605, 0x25CB, 0x25CF, 0x25CE, 0x25C7, 0x25C6, 0x25A1, 0x25A0, 0x25B3, 0x25B2, 0x25BD, 0x25BC, 0x2192, 0x2190, 0x2191, 0x2193, 0x2194, 0x3013, 0x226A, 0x226B, 0x221A, 0x223D, 0x221D, 0x2235, 0x222B, 0x222C, 0x2208, 0x220B, 0x2286, 0x2287, 0x2282, 0x2283, 0x222A, 0x2229, 0x2227, 0x2228, 0xFFE2, - 0xC910, 0xC912, 0xC913, 0xC914, 0xC915, 0xC916, 0xC917, 0xC919, 0xC91A, 0xC91B, 0xC91C, 0xC91D, 0xC91E, 0xC91F, 0xC920, 0xC921, 0xC922, 0xC923, 0xC924, 0xC925, 0xC926, 0xC927, 0xC928, 0xC929, 0xC92A, 0xC92B, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC92D, 0xC92E, 0xC92F, 0xC930, 0xC931, 0xC932, 0xC933, 0xC935, 0xC936, 0xC937, 0xC938, 0xC939, 0xC93A, 0xC93B, 0xC93C, 0xC93D, 0xC93E, 0xC93F, 0xC940, 0xC941, 0xC942, 0xC943, 0xC944, 0xC945, 0xC946, 0xC947, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC948, 0xC949, 0xC94A, 0xC94B, 0xC94C, 0xC94D, 0xC94E, 0xC94F, 0xC952, 0xC953, 0xC955, 0xC956, 0xC957, 0xC959, 0xC95A, 0xC95B, 0xC95C, 0xC95D, 0xC95E, 0xC95F, 0xC962, 0xC964, 0xC965, 0xC966, 0xC967, 0xC968, 0xC969, 0xC96A, 0xC96B, 0xC96D, 0xC96E, 0xC96F, 0x21D2, 0x21D4, 0x2200, 0x2203, 0x00B4, 0xFF5E, 0x02C7, 0x02D8, 0x02DD, 0x02DA, 0x02D9, 0x00B8, 0x02DB, 0x00A1, 0x00BF, 0x02D0, 0x222E, 0x2211, 0x220F, 0x00A4, 0x2109, 0x2030, 0x25C1, 0x25C0, 0x25B7, 0x25B6, 0x2664, 0x2660, 0x2661, 0x2665, 0x2667, 0x2663, 0x2299, 0x25C8, 0x25A3, 0x25D0, 0x25D1, 0x2592, 0x25A4, 0x25A5, 0x25A8, 0x25A7, 0x25A6, 0x25A9, 0x2668, 0x260F, 0x260E, 0x261C, 0x261E, 0x00B6, 0x2020, 0x2021, 0x2195, 0x2197, 0x2199, 0x2196, 0x2198, 0x266D, 0x2669, 0x266A, 0x266C, 0x327F, 0x321C, 0x2116, 0x33C7, 0x2122, 0x33C2, 0x33D8, 0x2121, 0x20AC, 0x00AE, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0xC910, 0xC912, 0xC913, 0xC914, 0xC915, 0xC916, 0xC917, 0xC919, 0xC91A, 0xC91B, 0xC91C, 0xC91D, 0xC91E, 0xC91F, 0xC920, 0xC921, 0xC922, 0xC923, 0xC924, 0xC925, 0xC926, 0xC927, 0xC928, 0xC929, 0xC92A, 0xC92B, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC92D, 0xC92E, 0xC92F, 0xC930, 0xC931, 0xC932, 0xC933, 0xC935, 0xC936, 0xC937, 0xC938, 0xC939, 0xC93A, 0xC93B, 0xC93C, 0xC93D, 0xC93E, 0xC93F, 0xC940, 0xC941, 0xC942, 0xC943, 0xC944, 0xC945, 0xC946, 0xC947, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC948, 0xC949, 0xC94A, 0xC94B, 0xC94C, 0xC94D, 0xC94E, 0xC94F, 0xC952, 0xC953, 0xC955, 0xC956, 0xC957, 0xC959, 0xC95A, 0xC95B, 0xC95C, 0xC95D, 0xC95E, 0xC95F, 0xC962, 0xC964, 0xC965, 0xC966, 0xC967, 0xC968, 0xC969, 0xC96A, 0xC96B, 0xC96D, 0xC96E, 0xC96F, 0x21D2, 0x21D4, 0x2200, 0x2203, 0x00B4, 0xFF5E, 0x02C7, 0x02D8, 0x02DD, 0x02DA, 0x02D9, 0x00B8, 0x02DB, 0x00A1, 0x00BF, 0x02D0, 0x222E, 0x2211, 0x220F, 0x00A4, 0x2109, 0x2030, 0x25C1, 0x25C0, 0x25B7, 0x25B6, 0x2664, 0x2660, 0x2661, 0x2665, 0x2667, 0x2663, 0x2299, 0x25C8, 0x25A3, 0x25D0, 0x25D1, 0x2592, 0x25A4, 0x25A5, 0x25A8, 0x25A7, 0x25A6, 0x25A9, 0x2668, 0x260F, 0x260E, 0x261C, 0x261E, 0x00B6, 0x2020, 0x2021, 0x2195, 0x2197, 0x2199, 0x2196, 0x2198, 0x266D, 0x2669, 0x266A, 0x266C, 0x327F, 0x321C, 0x2116, 0x33C7, 0x2122, 0x33C2, 0x33D8, 0x2121, 0x20AC, 0x00AE, 0x327E, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC971, 0xC972, 0xC973, 0xC975, 0xC976, 0xC977, 0xC978, 0xC979, 0xC97A, 0xC97B, 0xC97D, 0xC97E, 0xC97F, 0xC980, 0xC981, 0xC982, 0xC983, 0xC984, 0xC985, 0xC986, 0xC987, 0xC98A, 0xC98B, 0xC98D, 0xC98E, 0xC98F, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC991, 0xC992, 0xC993, 0xC994, 0xC995, 0xC996, 0xC997, 0xC99A, 0xC99C, 0xC99E, 0xC99F, 0xC9A0, 0xC9A1, 0xC9A2, 0xC9A3, 0xC9A4, 0xC9A5, 0xC9A6, 0xC9A7, 0xC9A8, 0xC9A9, 0xC9AA, 0xC9AB, 0xC9AC, 0xC9AD, 0xC9AE, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC9AF, 0xC9B0, 0xC9B1, 0xC9B2, 0xC9B3, 0xC9B4, 0xC9B5, 0xC9B6, 0xC9B7, 0xC9B8, 0xC9B9, 0xC9BA, 0xC9BB, 0xC9BC, 0xC9BD, 0xC9BE, 0xC9BF, 0xC9C2, 0xC9C3, 0xC9C5, 0xC9C6, 0xC9C9, 0xC9CB, 0xC9CC, 0xC9CD, 0xC9CE, 0xC9CF, 0xC9D2, 0xC9D4, 0xC9D7, 0xC9D8, 0xC9DB, 0xFF01, 0xFF02, 0xFF03, 0xFF04, 0xFF05, 0xFF06, 0xFF07, 0xFF08, 0xFF09, 0xFF0A, 0xFF0B, 0xFF0C, 0xFF0D, 0xFF0E, 0xFF0F, 0xFF10, 0xFF11, 0xFF12, 0xFF13, 0xFF14, 0xFF15, 0xFF16, 0xFF17, 0xFF18, 0xFF19, 0xFF1A, 0xFF1B, 0xFF1C, 0xFF1D, 0xFF1E, 0xFF1F, 0xFF20, 0xFF21, 0xFF22, 0xFF23, 0xFF24, 0xFF25, 0xFF26, 0xFF27, 0xFF28, 0xFF29, 0xFF2A, 0xFF2B, 0xFF2C, 0xFF2D, 0xFF2E, 0xFF2F, 0xFF30, 0xFF31, 0xFF32, 0xFF33, 0xFF34, 0xFF35, 0xFF36, 0xFF37, 0xFF38, 0xFF39, 0xFF3A, 0xFF3B, 0xFFE6, 0xFF3D, 0xFF3E, 0xFF3F, 0xFF40, 0xFF41, 0xFF42, 0xFF43, 0xFF44, 0xFF45, 0xFF46, 0xFF47, 0xFF48, 0xFF49, 0xFF4A, 0xFF4B, 0xFF4C, 0xFF4D, 0xFF4E, 0xFF4F, 0xFF50, 0xFF51, 0xFF52, 0xFF53, 0xFF54, 0xFF55, 0xFF56, 0xFF57, 0xFF58, 0xFF59, 0xFF5A, 0xFF5B, 0xFF5C, 0xFF5D, 0xFFE3, 0xC9DE, 0xC9DF, 0xC9E1, 0xC9E3, 0xC9E5, 0xC9E6, 0xC9E8, 0xC9E9, 0xC9EA, 0xC9EB, 0xC9EE, 0xC9F2, 0xC9F3, 0xC9F4, 0xC9F5, 0xC9F6, 0xC9F7, 0xC9FA, 0xC9FB, 0xC9FD, 0xC9FE, 0xC9FF, 0xCA01, 0xCA02, 0xCA03, 0xCA04, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCA05, 0xCA06, 0xCA07, 0xCA0A, 0xCA0E, 0xCA0F, 0xCA10, 0xCA11, 0xCA12, 0xCA13, 0xCA15, 0xCA16, 0xCA17, 0xCA19, 0xCA1A, 0xCA1B, 0xCA1C, 0xCA1D, 0xCA1E, 0xCA1F, 0xCA20, 0xCA21, 0xCA22, 0xCA23, 0xCA24, 0xCA25, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCA26, 0xCA27, 0xCA28, 0xCA2A, 0xCA2B, 0xCA2C, 0xCA2D, 0xCA2E, 0xCA2F, 0xCA30, 0xCA31, 0xCA32, 0xCA33, 0xCA34, 0xCA35, 0xCA36, 0xCA37, 0xCA38, 0xCA39, 0xCA3A, 0xCA3B, 0xCA3C, 0xCA3D, 0xCA3E, 0xCA3F, 0xCA40, 0xCA41, 0xCA42, 0xCA43, 0xCA44, 0xCA45, 0xCA46, 0x3131, 0x3132, 0x3133, 0x3134, 0x3135, 0x3136, 0x3137, 0x3138, 0x3139, 0x313A, 0x313B, 0x313C, 0x313D, 0x313E, 0x313F, 0x3140, 0x3141, 0x3142, 0x3143, 0x3144, 0x3145, 0x3146, 0x3147, 0x3148, 0x3149, 0x314A, 0x314B, 0x314C, 0x314D, 0x314E, 0x314F, 0x3150, 0x3151, 0x3152, 0x3153, 0x3154, 0x3155, 0x3156, 0x3157, 0x3158, 0x3159, 0x315A, 0x315B, 0x315C, 0x315D, 0x315E, 0x315F, 0x3160, 0x3161, 0x3162, 0x3163, 0x3164, 0x3165, 0x3166, 0x3167, 0x3168, 0x3169, 0x316A, 0x316B, 0x316C, 0x316D, 0x316E, 0x316F, 0x3170, 0x3171, 0x3172, 0x3173, 0x3174, 0x3175, 0x3176, 0x3177, 0x3178, 0x3179, 0x317A, 0x317B, 0x317C, 0x317D, 0x317E, 0x317F, 0x3180, 0x3181, 0x3182, 0x3183, 0x3184, 0x3185, 0x3186, 0x3187, 0x3188, 0x3189, 0x318A, 0x318B, 0x318C, 0x318D, 0x318E, 0xCA47, 0xCA48, 0xCA49, 0xCA4A, 0xCA4B, 0xCA4E, 0xCA4F, 0xCA51, 0xCA52, 0xCA53, 0xCA55, 0xCA56, 0xCA57, 0xCA58, 0xCA59, 0xCA5A, 0xCA5B, 0xCA5E, 0xCA62, 0xCA63, 0xCA64, 0xCA65, 0xCA66, 0xCA67, 0xCA69, 0xCA6A, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCA6B, 0xCA6C, 0xCA6D, 0xCA6E, 0xCA6F, 0xCA70, 0xCA71, 0xCA72, 0xCA73, 0xCA74, 0xCA75, 0xCA76, 0xCA77, 0xCA78, 0xCA79, 0xCA7A, 0xCA7B, 0xCA7C, 0xCA7E, 0xCA7F, 0xCA80, 0xCA81, 0xCA82, 0xCA83, 0xCA85, 0xCA86, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCA87, 0xCA88, 0xCA89, 0xCA8A, 0xCA8B, 0xCA8C, 0xCA8D, 0xCA8E, 0xCA8F, 0xCA90, 0xCA91, 0xCA92, 0xCA93, 0xCA94, 0xCA95, 0xCA96, 0xCA97, 0xCA99, 0xCA9A, 0xCA9B, 0xCA9C, 0xCA9D, 0xCA9E, 0xCA9F, 0xCAA0, 0xCAA1, 0xCAA2, 0xCAA3, 0xCAA4, 0xCAA5, 0xCAA6, 0xCAA7, 0x2170, 0x2171, 0x2172, 0x2173, 0x2174, 0x2175, 0x2176, 0x2177, 0x2178, 0x2179, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x2160, 0x2161, 0x2162, 0x2163, 0x2164, 0x2165, 0x2166, 0x2167, 0x2168, 0x2169, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, 0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F, 0x03A0, 0x03A1, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7, 0x03B8, 0x03B9, 0x03BA, 0x03BB, 0x03BC, 0x03BD, 0x03BE, 0x03BF, 0x03C0, 0x03C1, 0x03C3, 0x03C4, 0x03C5, 0x03C6, 0x03C7, 0x03C8, 0x03C9, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index 92cd419115..8e7039b5de 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -22,10 +22,13 @@ namespace { constexpr int CES_ERROR_INVALID_PARAMETER = static_cast(0x805C0001); +constexpr int CES_ERROR_INVALID_PROFILE = static_cast(0x805C0004); constexpr int CES_ERROR_INVALID_SRC_BUFFER = static_cast(0x805C0010); constexpr int CES_ERROR_SRC_BUFFER_END = static_cast(0x805C0011); constexpr int CES_ERROR_INVALID_ENCODE = static_cast(0x805C0014); +constexpr int CES_ERROR_ILLEGAL_CODE = static_cast(0x805C0015); constexpr int CES_ERROR_UNASSIGNED_CODE = static_cast(0x805C0020); +constexpr int CES_ERROR_UNSUPPORTED_CODE = static_cast(0x805C0024); constexpr int CES_ERROR_INVALID_DST_BUFFER = static_cast(0x805C0030); constexpr int CES_ERROR_DST_BUFFER_END = static_cast(0x805C0031); @@ -35,13 +38,13 @@ constexpr const uint8_t* CP1251_PROFILE = PROFILES[1]; constexpr const uint8_t* CP1250_PROFILE = PROFILES[2]; constexpr uint16_t CP1252_HIGH[32] = { - 0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008D, 0x017D, 0x008F, - 0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x009D, 0x017E, 0x0178, + 0x20AC, 0x0000, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x0000, 0x017D, 0x0000, + 0x0000, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x0000, 0x017E, 0x0178, }; constexpr uint16_t CP1251_HIGH[128] = { 0x0402, 0x0403, 0x201A, 0x0453, 0x201E, 0x2026, 0x2020, 0x2021, 0x20AC, 0x2030, 0x0409, 0x2039, 0x040A, 0x040C, 0x040B, 0x040F, - 0x0452, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x0098, 0x2122, 0x0459, 0x203A, 0x045A, 0x045C, 0x045B, 0x045F, + 0x0452, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x0000, 0x2122, 0x0459, 0x203A, 0x045A, 0x045C, 0x045B, 0x045F, 0x00A0, 0x040E, 0x045E, 0x0408, 0x00A4, 0x0490, 0x00A6, 0x00A7, 0x0401, 0x00A9, 0x0404, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x0407, 0x00B0, 0x00B1, 0x0406, 0x0456, 0x0491, 0x00B5, 0x00B6, 0x00B7, 0x0451, 0x2116, 0x0454, 0x00BB, 0x0458, 0x0405, 0x0455, 0x0457, 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, @@ -51,8 +54,8 @@ constexpr uint16_t CP1251_HIGH[128] = { }; constexpr uint16_t CP1250_HIGH[128] = { - 0x20AC, 0x0081, 0x201A, 0x0083, 0x201E, 0x2026, 0x2020, 0x2021, 0x0088, 0x2030, 0x0160, 0x2039, 0x015A, 0x0164, 0x017D, 0x0179, - 0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x0098, 0x2122, 0x0161, 0x203A, 0x015B, 0x0165, 0x017E, 0x017A, + 0x20AC, 0x0000, 0x201A, 0x0000, 0x201E, 0x2026, 0x2020, 0x2021, 0x0000, 0x2030, 0x0160, 0x2039, 0x015A, 0x0164, 0x017D, 0x0179, + 0x0000, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x0000, 0x2122, 0x0161, 0x203A, 0x015B, 0x0165, 0x017E, 0x017A, 0x00A0, 0x02C7, 0x02D8, 0x0141, 0x00A4, 0x0104, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x015E, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x017B, 0x00B0, 0x00B1, 0x02DB, 0x0142, 0x00B4, 0x00B5, 0x00B6, 0x00B7, 0x00B8, 0x0105, 0x015F, 0x00BB, 0x013D, 0x02DD, 0x013E, 0x017C, 0x0154, 0x00C1, 0x00C2, 0x0102, 0x00C4, 0x0139, 0x0106, 0x00C7, 0x010C, 0x00C9, 0x0118, 0x00CB, 0x011A, 0x00CD, 0x00CE, 0x010E, @@ -80,9 +83,12 @@ const uint16_t* sbcHighTable(const void* profile) { return nullptr; } -uint32_t sbcToUnicode(const uint8_t* profile, uint8_t sbc) { - if (const uint16_t* high = sbcHighTable(profile)) return sbc >= 0x80 ? high[sbc - 0x80] : sbc; - return sbc >= 0x80 && sbc < 0xA0 ? CP1252_HIGH[sbc - 0x80] : sbc; +bool sbcToUnicode(const uint8_t* profile, uint8_t sbc, uint32_t& code) { + code = sbc; + if (sbc < 0x80) return true; + if (const uint16_t* high = sbcHighTable(profile)) code = high[sbc - 0x80]; + else if (sbc < 0xA0) code = CP1252_HIGH[sbc - 0x80]; + return code != 0; } bool unicodeToSbc(const uint8_t* profile, uint32_t code, uint8_t& sbc) { @@ -112,25 +118,32 @@ bool unicodeToSbc(const uint8_t* profile, uint32_t code, uint8_t& sbc) { return false; } -int decodeUtf8(const uint8_t* utf8, uint32_t utf8max, uint32_t& length, uint32_t& code) { +int decodeScalarUtf8(const uint8_t* utf8, uint32_t utf8max, uint32_t& length, uint32_t& code) { const uint8_t lead = utf8[0]; + length = 0; uint32_t minimum = 0; if (lead < 0x80) { length = 1; code = lead; return 0; } - if ((lead & 0xE0) == 0xC0) { length = 2; code = lead & 0x1F; minimum = 0x80; } - else if ((lead & 0xF0) == 0xE0) { length = 3; code = lead & 0x0F; minimum = 0x800; } - else if ((lead & 0xF8) == 0xF0) { length = 4; code = lead & 0x07; minimum = 0x10000; } - else return CES_ERROR_INVALID_ENCODE; - - for (uint32_t i = 1; i < length; ++i) { - if (i >= utf8max) return CES_ERROR_SRC_BUFFER_END; - if ((utf8[i] & 0xC0) != 0x80) return CES_ERROR_INVALID_ENCODE; + if (lead < 0xC0 || lead >= 0xFE) return CES_ERROR_INVALID_ENCODE; + if (lead < 0xE0) { length = 2; code = lead & 0x1F; minimum = 0x80; } + else if (lead < 0xF0) { length = 3; code = lead & 0x0F; minimum = 0x800; } + else if (lead < 0xF8) { length = 4; code = lead & 0x07; minimum = 0x10000; } + else if (lead < 0xFC) { length = 5; code = lead & 0x03; minimum = 0x200000; } + else { length = 6; code = lead & 0x01; minimum = 0x4000000; } + const bool truncated = utf8max < length; + const uint32_t present = truncated ? utf8max : length; + for (uint32_t i = 1; i < present; ++i) { + if ((utf8[i] & 0xC0) != 0x80) { + length = truncated ? i + 1 : i; + return CES_ERROR_INVALID_ENCODE; + } code = code << 6 | (utf8[i] & 0x3F); } - if (code < minimum || code > 0x10FFFF || (code >= 0xD800 && code <= 0xDFFF)) return CES_ERROR_INVALID_ENCODE; + if (truncated) return CES_ERROR_SRC_BUFFER_END; + if (code < minimum || (code >= 0xD800 && code <= 0xDFFF)) return CES_ERROR_ILLEGAL_CODE; return 0; } @@ -181,7 +194,7 @@ bool hasSs2(const void* profile) { } bool hasSs3(const void* profile) { - return hasTag(profile, EUC_JP_TAG) || hasTag(profile, EUC_JP_X0208_SS3_TAG); + return hasTag(profile, EUC_JP_X0208_SS3_TAG); } bool isSjisX0208(const void* profile) { @@ -197,10 +210,11 @@ struct JisChange { uint16_t code; }; -constexpr JisChange JIS_X0208_CHANGES[6] = {{0x2141, 0x301C}, {0x2142, 0x2016}, {0x215D, 0x2212}, {0x2171, 0x00A2}, {0x2172, 0x00A3}, {0x224C, 0x00AC}}; -constexpr uint16_t JIS_X0208_UNSETTLED_CELL = 0x2140; -constexpr uint16_t JIS_X0208_UNSETTLED_CODE = 0xFF3C; -constexpr const char* JIS_X0208_UNSETTLED = "libSceCesCs: the console's mapping of JIS X 0208 cell 0x2140 (U+005C or U+FF3C) is unknown"; +constexpr JisChange JIS_X0208_CHANGES[7] = {{0x213D, 0x2014}, {0x2141, 0x301C}, {0x2142, 0x2016}, {0x215D, 0x2212}, {0x2171, 0x00A2}, {0x2172, 0x00A3}, {0x224C, 0x00AC}}; +constexpr uint16_t JIS_X0208_BACKSLASH_CELL = 0x2140; +constexpr JisChange JIS_X0208_EUC_FALLBACKS[2] = {{0x216F, 0x00A5}, {0x2131, 0x203E}}; + +enum class JisStrict { None, Sjis, Euc }; constexpr uint32_t jisIndex(uint16_t jis) { return ((jis >> 8) - 0x21) * 94 + (jis & 0xFF) - 0x21; @@ -210,22 +224,21 @@ bool isCp932ExtensionRow(uint32_t row) { return row == 13 || (row >= 89 && row <= 92); } -uint16_t strictJisX0208(uint32_t index) { - if (isCp932ExtensionRow(index / 94 + 1) || index == jisIndex(JIS_X0208_UNSETTLED_CELL)) return 0; +uint16_t strictJisX0208(JisStrict strict, uint32_t index) { + if (isCp932ExtensionRow(index / 94 + 1)) return 0; + if (index == jisIndex(JIS_X0208_BACKSLASH_CELL)) return strict == JisStrict::Sjis ? 0x005C : 0xFF3C; for (const JisChange& change : JIS_X0208_CHANGES) { if (jisIndex(change.jis) == index) return change.code; } return JisX0208::TO_UCS2[index]; } -uint32_t jisX0208ToUnicode(bool strict, uint32_t index) { - if (!strict) return JisX0208::TO_UCS2[index]; - if (index == jisIndex(JIS_X0208_UNSETTLED_CELL)) throw std::runtime_error(JIS_X0208_UNSETTLED); - return strictJisX0208(index); +uint32_t jisX0208ToUnicode(JisStrict strict, uint32_t index) { + return strict == JisStrict::None ? JisX0208::TO_UCS2[index] : strictJisX0208(strict, index); } -void requireSettledSjisX0208(uint32_t code) { - if (code == 0x5C || code == 0x7E || code == 0xA5 || code == 0x203E) throw std::runtime_error("libSceCesCs: the console's mapping of Shift-JIS bytes 0x5C and 0x7E (ASCII or JIS X 0201 Roman) is unknown"); +uint32_t sjisX0208Single(uint8_t byte) { + return byte == 0x5C ? 0x00A5 : byte == 0x7E ? 0x203E : byte; } constexpr uint8_t EUC_CN_TAG[8] = {'E', 'U', 'C', 'C', 'N', '9', '3', '6'}; @@ -243,6 +256,36 @@ bool isSbc(const void* profile) { return profile == CP1252_PROFILE || sbcHighTable(profile) != nullptr; } +int rejectLead(uint32_t& length) { + length = 1; + return CES_ERROR_INVALID_ENCODE; +} + +int needBytes(uint32_t size, uint32_t& length) { + length = size; + return CES_ERROR_SRC_BUFFER_END; +} + +int rejectTrail(uint8_t trail, uint32_t size, uint32_t& length) { + length = trail == 0 ? 1 : size; + return CES_ERROR_INVALID_ENCODE; +} + +bool stopsAtNul(const uint8_t* mbcs, uint32_t mbcsMax, uint32_t from, uint32_t size, uint32_t& length) { + for (uint32_t index = from; index < size && index < mbcsMax; ++index) { + if (mbcs[index] == 0) { + length = index; + return true; + } + } + return false; +} + +int unassigned(uint32_t size, uint32_t& length) { + length = size; + return CES_ERROR_UNASSIGNED_CODE; +} + int decodeGb(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t& length, uint32_t& code) { const uint8_t lead = mbcs[0]; if (lead < 0x80) { @@ -256,12 +299,12 @@ int decodeGb(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_ code = 0x20AC; return 0; } - if (lead == 0xFF || (!gbk && lead < 0xA1)) return CES_ERROR_INVALID_ENCODE; - if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; + if (lead == 0xFF || (!gbk && lead < 0xA1)) return rejectLead(length); + if (mbcsMax < 2) return needBytes(2, length); const uint8_t trail = mbcs[1]; - if (trail == 0xFF || (gbk ? trail < 0x40 || trail == 0x7F : trail < 0xA1)) return CES_ERROR_INVALID_ENCODE; + if (trail == 0xFF || (gbk ? trail < 0x40 || trail == 0x7F : trail < 0xA1)) return rejectTrail(trail, 2, length); code = Cp936::TO_UCS2[(lead - 0x81) * 190 + (trail < 0x7F ? trail - 0x40 : trail - 0x41)]; - if (code == 0) return CES_ERROR_UNASSIGNED_CODE; + if (code == 0) return unassigned(2, length); length = 2; return 0; } @@ -359,20 +402,21 @@ int decodeGb18030(const Gb18030Tables& tables, const uint8_t* mbcs, uint32_t mbc code = lead; return 0; } - if (lead == 0x80 || lead == 0xFF) return CES_ERROR_INVALID_ENCODE; - if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; + if (lead == 0x80 || lead == 0xFF) return rejectLead(length); + if (mbcsMax < 2) return needBytes(2, length); const uint8_t second = mbcs[1]; if (second >= 0x30 && second <= 0x39) { - if (mbcsMax < 4) return CES_ERROR_SRC_BUFFER_END; - if (mbcs[2] < 0x81 || mbcs[2] == 0xFF || mbcs[3] < 0x30 || mbcs[3] > 0x39) return CES_ERROR_INVALID_ENCODE; + if (stopsAtNul(mbcs, mbcsMax, 2, 4, length)) return CES_ERROR_INVALID_ENCODE; + if (mbcsMax < 4) return needBytes(4, length); + if (mbcs[2] < 0x81 || mbcs[2] == 0xFF || mbcs[3] < 0x30 || mbcs[3] > 0x39) return rejectTrail(mbcs[2], 4, length); const uint32_t linear = (((lead - 0x81) * 10 + (second - 0x30)) * 126 + (mbcs[2] - 0x81)) * 10 + (mbcs[3] - 0x30); const uint32_t* run = findRun(tables.fourByte, linear); - if (!run) return CES_ERROR_UNASSIGNED_CODE; + if (!run) return unassigned(4, length); length = 4; code = run[1] + linear - run[0]; return 0; } - if (second < 0x40 || second == 0x7F || second == 0xFF) return CES_ERROR_INVALID_ENCODE; + if (second < 0x40 || second == 0x7F || second == 0xFF) return rejectTrail(second, 2, length); length = 2; code = gb18030Cell(tables, (lead - 0x81) * 190 + (second < 0x7F ? second - 0x40 : second - 0x41)); return 0; @@ -463,7 +507,7 @@ const void* contextProfile(const void* context) { constexpr uint8_t EUC_KR_TAG[8] = {'E', 'U', 'C', 'K', 'R', '9', '4', '9'}; constexpr uint8_t UHC_TAG[8] = {'U', 'H', 'C', 'C', 'P', '9', '4', '9'}; constexpr uint8_t BIG5_TAG[8] = {'B', 'I', 'G', '5', 'C', '9', '5', '0'}; -constexpr uint16_t BIG5_LAST_CELL[6] = {0x2550, 0x255E, 0x2561, 0x256A, 0x5341, 0x5345}; +constexpr uint16_t BIG5_LAST_CELL[2] = {0x5341, 0x5345}; bool isEucKr(const void* profile) { return profile != nullptr && std::memcmp(profile, EUC_KR_TAG, sizeof(EUC_KR_TAG)) == 0; @@ -488,11 +532,11 @@ int decodeCp949(bool unified, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t& l code = lead; return 0; } - if (lead < (unified ? 0x81 : 0xA1) || lead == 0xFF) return CES_ERROR_INVALID_ENCODE; - if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; + if (lead < (unified ? 0x81 : 0xA1) || lead == 0xFF) return rejectLead(length); + if (mbcsMax < 2) return needBytes(2, length); const uint8_t trail = mbcs[1]; const bool extended = unified && ((trail >= 0x41 && trail <= 0x5A) || (trail >= 0x61 && trail <= 0x7A) || (trail >= 0x81 && trail <= 0xA0)); - if (!extended && (trail < 0xA1 || trail == 0xFF)) return CES_ERROR_INVALID_ENCODE; + if (!extended && (trail < 0xA1 || trail == 0xFF)) return rejectTrail(trail, 2, length); length = 2; code = Cp949::TO_UCS2[(lead - 0x81) * 190 + (trail - 0x41)]; return code != 0 ? 0 : CES_ERROR_UNASSIGNED_CODE; @@ -505,11 +549,11 @@ int decodeCp950(const uint8_t* mbcs, uint32_t mbcsMax, uint32_t& length, uint32_ code = lead; return 0; } - if (lead == 0x80 || lead == 0xFF) return CES_ERROR_INVALID_ENCODE; - if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; + if (lead == 0x80 || lead == 0xFF) return rejectLead(length); + if (mbcsMax < 2) return needBytes(2, length); const uint8_t trail = mbcs[1]; - if (trail < 0x40 || (trail > 0x7E && trail < 0xA1) || trail == 0xFF) return CES_ERROR_INVALID_ENCODE; - if (lead < 0xA1 || lead > 0xF9) return CES_ERROR_UNASSIGNED_CODE; + if (trail < 0x40 || (trail > 0x7E && trail < 0xA1) || trail == 0xFF) return rejectTrail(trail, 2, length); + if (lead < 0xA1 || lead > 0xF9) return unassigned(2, length); length = 2; code = Cp950::TO_UCS2[(lead - 0xA1) * 157 + (trail < 0xA1 ? trail - 0x40 : trail - 0xA1 + 63)]; return code != 0 ? 0 : CES_ERROR_UNASSIGNED_CODE; @@ -570,6 +614,80 @@ int encodeDbcs(const std::vector& reverse, bool euc, uint32_t code, ui return 0; } +bool jisX0212RowEmpty(uint8_t row) { + for (uint32_t cell = 0; cell < 94; ++cell) { + if (JisX0212::TO_UCS2[(row - 0xA1) * 94 + cell] != 0) return false; + } + return true; +} + +int decodeEucJpScalar(const uint8_t* euc, uint32_t eucMax, uint32_t& length, uint32_t& code) { + const uint8_t lead = euc[0]; + length = 1; + if (lead == 0x0E || lead == 0xA0 || lead == 0xFF) return CES_ERROR_INVALID_ENCODE; + if (lead < 0xA0 && lead != 0x8E && lead != 0x8F) { + code = lead; + return 0; + } + const uint32_t needed = lead == 0x8F ? 3 : 2; + for (uint32_t i = 1; i < needed && i < eucMax; ++i) { + if (euc[i] == 0) { + length = i; + return CES_ERROR_INVALID_ENCODE; + } + } + length = needed; + if (eucMax < needed) return CES_ERROR_SRC_BUFFER_END; + const uint8_t trail = euc[1]; + if (lead == 0x8E) { + if (trail >= 0xA1 && trail <= 0xDF) { + code = 0xFF61 + (trail - 0xA1); + return 0; + } + return trail >= 0xE0 && trail <= 0xFE ? CES_ERROR_UNASSIGNED_CODE : CES_ERROR_INVALID_ENCODE; + } + if (lead == 0x8F) { + if (trail < 0xA1 || trail > 0xFE || euc[2] < 0xA1 || euc[2] > 0xFE) return CES_ERROR_INVALID_ENCODE; + return jisX0212RowEmpty(trail) ? CES_ERROR_UNASSIGNED_CODE : CES_ERROR_INVALID_ENCODE; + } + if (trail < 0xA1 || trail > 0xFE) return CES_ERROR_INVALID_ENCODE; + code = JisX0208::TO_UCS2[(lead - 0xA1) * 94 + (trail - 0xA1)]; + return code != 0 ? 0 : CES_ERROR_UNASSIGNED_CODE; +} + +int decodeEucJp(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t& length, uint32_t& code) { + if (hasTag(profile, EUC_JP_TAG)) return decodeEucJpScalar(mbcs, mbcsMax, length, code); + const uint8_t lead = mbcs[0]; + if (lead < 0x80) { + length = 1; + code = lead; + return 0; + } + if (lead == 0x8E && hasSs2(profile)) { + if (mbcsMax < 2) return needBytes(2, length); + if (mbcs[1] < 0xA1 || mbcs[1] > 0xDF) return rejectTrail(mbcs[1], 2, length); + length = 2; + code = 0xFF61 + (mbcs[1] - 0xA1); + return 0; + } + if (lead >= 0xA1 && lead <= 0xFE) { + if (mbcsMax < 2) return needBytes(2, length); + if (mbcs[1] < 0xA1 || mbcs[1] > 0xFE) return rejectTrail(mbcs[1], 2, length); + length = 2; + code = jisX0208ToUnicode(isEucJpX0208(profile) ? JisStrict::Euc : JisStrict::None, (lead - 0xA1) * 94 + (mbcs[1] - 0xA1)); + return code != 0 ? 0 : CES_ERROR_UNASSIGNED_CODE; + } + if (lead == 0x8F && hasSs3(profile)) { + if (stopsAtNul(mbcs, mbcsMax, 1, 3, length)) return CES_ERROR_INVALID_ENCODE; + if (mbcsMax < 3) return needBytes(3, length); + if (mbcs[1] < 0xA1 || mbcs[1] > 0xFE || mbcs[2] < 0xA1 || mbcs[2] > 0xFE) return rejectTrail(mbcs[1], 3, length); + length = 3; + code = JisX0212::TO_UCS2[(mbcs[1] - 0xA1) * 94 + (mbcs[2] - 0xA1)]; + return code != 0 ? 0 : CES_ERROR_UNASSIGNED_CODE; + } + return rejectLead(length); +} + int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t& length, uint32_t& code) { if (isEucKr(profile) || isUhc(profile)) return decodeCp949(isUhc(profile), mbcs, mbcsMax, length, code); if (isBig5(profile)) return decodeCp950(mbcs, mbcsMax, length, code); @@ -578,15 +696,13 @@ int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint3 const uint8_t lead = mbcs[0]; if (isSbc(profile)) { length = 1; - code = sbcToUnicode(static_cast(profile), lead); - return 0; + return sbcToUnicode(static_cast(profile), lead, code) ? 0 : CES_ERROR_UNASSIGNED_CODE; } if (isSjis(profile)) { const bool strict = isSjisX0208(profile); if (lead < 0x80) { - if (strict) requireSettledSjisX0208(lead); length = 1; - code = lead; + code = strict ? sjisX0208Single(lead) : lead; return 0; } if (lead >= 0xA1 && lead <= 0xDF) { @@ -595,9 +711,9 @@ int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint3 return 0; } if ((lead >= 0x81 && lead <= 0x9F) || (lead >= 0xE0 && lead <= 0xFC)) { - if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; + if (mbcsMax < 2) return needBytes(2, length); const uint8_t trail = mbcs[1]; - if (trail < 0x40 || trail == 0x7F || trail > 0xFC) return CES_ERROR_INVALID_ENCODE; + if (trail < 0x40 || trail == 0x7F || trail > 0xFC) return rejectTrail(trail, 2, length); if (lead >= 0xFA && !strict) { const uint32_t trailIdx = trail < 0x7F ? trail - 0x40 : trail - 0x41; const uint32_t offset = (lead - 0xFA) * 188 + trailIdx; @@ -611,50 +727,24 @@ int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint3 const int row = 89 + kanjiIdx / 94; const int cell = 1 + kanjiIdx % 94; code = JisX0208::TO_UCS2[(row - 1) * 94 + (cell - 1)]; - if (code == 0) return CES_ERROR_UNASSIGNED_CODE; + if (code == 0) return unassigned(2, length); length = 2; return 0; } - return CES_ERROR_UNASSIGNED_CODE; + return unassigned(2, length); } int row = 0; int cell = 0; sjisToJis(lead, trail, row, cell); - if (row < 1 || row > 94 || cell < 1 || cell > 94) return CES_ERROR_UNASSIGNED_CODE; - code = jisX0208ToUnicode(strict, (row - 1) * 94 + (cell - 1)); - if (code == 0) return CES_ERROR_UNASSIGNED_CODE; + if (row < 1 || row > 94 || cell < 1 || cell > 94) return unassigned(2, length); + code = jisX0208ToUnicode(strict ? JisStrict::Sjis : JisStrict::None, (row - 1) * 94 + (cell - 1)); + if (code == 0) return unassigned(2, length); length = 2; return 0; } - return CES_ERROR_INVALID_ENCODE; + return rejectLead(length); } - if (lead < 0x80) { - length = 1; - code = lead; - return 0; - } - if (lead == 0x8E && hasSs2(profile)) { - if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; - if (mbcs[1] < 0xA1 || mbcs[1] > 0xDF) return CES_ERROR_INVALID_ENCODE; - length = 2; - code = 0xFF61 + (mbcs[1] - 0xA1); - return 0; - } - if (lead >= 0xA1 && lead <= 0xFE) { - if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; - if (mbcs[1] < 0xA1 || mbcs[1] > 0xFE) return CES_ERROR_INVALID_ENCODE; - length = 2; - code = jisX0208ToUnicode(isEucJpX0208(profile), (lead - 0xA1) * 94 + (mbcs[1] - 0xA1)); - return code != 0 ? 0 : CES_ERROR_UNASSIGNED_CODE; - } - if (lead == 0x8F && hasSs3(profile)) { - if (mbcsMax < 3) return CES_ERROR_SRC_BUFFER_END; - if (mbcs[1] < 0xA1 || mbcs[1] > 0xFE || mbcs[2] < 0xA1 || mbcs[2] > 0xFE) return CES_ERROR_INVALID_ENCODE; - length = 3; - code = JisX0212::TO_UCS2[(mbcs[1] - 0xA1) * 94 + (mbcs[2] - 0xA1)]; - return code != 0 ? 0 : CES_ERROR_UNASSIGNED_CODE; - } - return CES_ERROR_INVALID_ENCODE; + return decodeEucJp(profile, mbcs, mbcsMax, length, code); } struct JisCell { @@ -663,13 +753,16 @@ struct JisCell { bool supplementary; }; -std::vector buildJisReverse(bool strict) { +std::vector buildJisReverse(JisStrict strict) { std::vector entries; entries.reserve(2 * 94 * 94); for (uint32_t cell = 0; cell < 94 * 94; ++cell) { - const uint16_t code = strict ? strictJisX0208(cell) : JisX0208::TO_UCS2[cell]; + const uint16_t code = static_cast(jisX0208ToUnicode(strict, cell)); if (code != 0) entries.push_back({code, static_cast(cell), false}); } + if (strict == JisStrict::Euc) { + for (const JisChange& fallback : JIS_X0208_EUC_FALLBACKS) entries.push_back({fallback.code, static_cast(jisIndex(fallback.jis)), false}); + } for (uint32_t cell = 0; cell < 94 * 94; ++cell) { if (JisX0212::TO_UCS2[cell] != 0) entries.push_back({JisX0212::TO_UCS2[cell], static_cast(cell), true}); } @@ -678,13 +771,26 @@ std::vector buildJisReverse(bool strict) { return entries; } -const std::vector& jisReverse(bool strict) { - if (strict) { - static const auto strictReverse = buildJisReverse(true); - return strictReverse; +const std::vector& jisReverse(JisStrict strict) { + static const auto reverse = buildJisReverse(JisStrict::None); + static const auto sjisReverse = buildJisReverse(JisStrict::Sjis); + static const auto eucReverse = buildJisReverse(JisStrict::Euc); + return strict == JisStrict::Sjis ? sjisReverse : strict == JisStrict::Euc ? eucReverse : reverse; +} + +uint32_t jisAlias(uint32_t code) { + switch (code) { + case 0x00A2: return 0xFFE0; + case 0x00A3: return 0xFFE1; + case 0x00A5: return 0xFFE5; + case 0x00AC: return 0xFFE2; + case 0x2014: return 0x2015; + case 0x2016: return 0x2225; + case 0x203E: return 0xFFE3; + case 0x2212: return 0xFF0D; + case 0x301C: return 0xFF5E; + default: return code; } - static const auto reverse = buildJisReverse(false); - return reverse; } int encodeMbcs(const void* profile, uint32_t code, uint8_t* mbcs, uint32_t& length) { @@ -698,31 +804,47 @@ int encodeMbcs(const void* profile, uint32_t code, uint8_t* mbcs, uint32_t& leng } if (isSjis(profile)) { const bool strict = isSjisX0208(profile); - if (strict) requireSettledSjisX0208(code); - if (code < 0x80) { + if (strict && (code == 0x00A5 || code == 0x203E)) { + length = 1; + mbcs[0] = code == 0x00A5 ? 0x5C : 0x7E; + return 0; + } + if (code < 0x80 && !(strict && (code == 0x5C || code == 0x7E))) { length = 1; mbcs[0] = static_cast(code); return 0; } + if (!strict && code <= 0x9F) return CES_ERROR_UNSUPPORTED_CODE; + if (!strict && code == 0xFEFF) { + length = 0; + return 0; + } + if (!strict) code = jisAlias(code); if (code >= 0xFF61 && code <= 0xFF9F) { length = 1; mbcs[0] = static_cast(0xA1 + (code - 0xFF61)); return 0; } if (code > 0xFFFF) return CES_ERROR_UNASSIGNED_CODE; - if (strict && code == JIS_X0208_UNSETTLED_CODE) throw std::runtime_error(JIS_X0208_UNSETTLED); - const auto& reverse = jisReverse(strict); + const auto& reverse = jisReverse(strict ? JisStrict::Sjis : JisStrict::None); const auto found = std::lower_bound(reverse.begin(), reverse.end(), static_cast(code), [](const JisCell& entry, uint16_t value) { return entry.code < value; }); if (found == reverse.end() || found->code != code || found->supplementary) return CES_ERROR_UNASSIGNED_CODE; jisToSjis(found->cell / 94 + 1, found->cell % 94 + 1, mbcs[0], mbcs[1]); length = 2; return 0; } - if (code < 0x80) { + const bool strict = isEucJpX0208(profile); + if (!strict && code == 0x0E) return CES_ERROR_UNASSIGNED_CODE; + if (code < 0x80 || (!strict && code < 0xA0 && code != 0x8E && code != 0x8F)) { length = 1; mbcs[0] = static_cast(code); return 0; } + if (!strict && code == 0xFEFF) { + length = 0; + return 0; + } + if (!strict) code = jisAlias(code); if (code >= 0xFF61 && code <= 0xFF9F) { if (!hasSs2(profile)) return CES_ERROR_UNASSIGNED_CODE; length = 2; @@ -731,9 +853,7 @@ int encodeMbcs(const void* profile, uint32_t code, uint8_t* mbcs, uint32_t& leng return 0; } if (code > 0xFFFF) return CES_ERROR_UNASSIGNED_CODE; - const bool strict = isEucJpX0208(profile); - if (strict && code == JIS_X0208_UNSETTLED_CODE) throw std::runtime_error(JIS_X0208_UNSETTLED); - const auto& reverse = jisReverse(strict); + const auto& reverse = jisReverse(strict ? JisStrict::Euc : JisStrict::None); const auto found = std::lower_bound(reverse.begin(), reverse.end(), static_cast(code), [](const JisCell& entry, uint16_t value) { return entry.code < value; }); if (found == reverse.end() || found->code != code || (found->supplementary && !hasSs3(profile))) return CES_ERROR_UNASSIGNED_CODE; if (found->supplementary) { @@ -752,7 +872,7 @@ int encodeMbcs(const void* profile, uint32_t code, uint8_t* mbcs, uint32_t& leng int decodeUcs2(const uint16_t* ucs2, uint32_t, uint32_t& length, uint32_t& code) { length = 1; code = ucs2[0]; - return code >= 0xD800 && code <= 0xDFFF ? CES_ERROR_INVALID_ENCODE : 0; + return code >= 0xD800 && code <= 0xDFFF ? CES_ERROR_ILLEGAL_CODE : 0; } uint32_t encodeUcs2(uint32_t code, uint16_t* ucs2) { @@ -771,58 +891,50 @@ uint32_t encodeUtf16(uint32_t code, uint16_t* utf16) { return 2; } -template -int toMbcs(const void* context, const Source* source, uint32_t sourceMax, uint32_t* sourceLen, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, Decode decode) { +template +int convertString(const void* context, const Source* source, uint32_t sourceMax, uint32_t* sourceLen, Target* target, uint32_t targetMax, uint32_t* targetLen, bool counting, Decode decode, Encode encode) { + if (sourceLen) *sourceLen = 0; + if (targetLen) *targetLen = 0; + if (target && targetMax > 0) target[0] = 0; if (!context) return CES_ERROR_INVALID_PARAMETER; const void* profile = contextProfile(context); if (!isMbcs(profile)) return CES_ERROR_INVALID_PARAMETER; if (!source) return CES_ERROR_INVALID_SRC_BUFFER; + if (!counting && !target) return CES_ERROR_INVALID_DST_BUFFER; uint32_t read = 0; uint32_t written = 0; - while (read < sourceMax && source[read] != 0) { + int result = 0; + while ((sourceMax == 0 || read < sourceMax) && source[read] != 0) { uint32_t length = 0; uint32_t code = 0; - if (const int result = decode(source + read, sourceMax - read, length, code); result != 0) return result; - uint8_t encoded[4]; - uint32_t size = 0; - if (const int result = encodeMbcs(profile, code, encoded, size); result != 0) return result; - if (mbcs) { - if (written + size > mbcsMax) return CES_ERROR_DST_BUFFER_END; - std::memcpy(mbcs + written, encoded, size); - } - read += length; - written += size; - } - if (sourceLen) *sourceLen = read; - if (mbcsLen) *mbcsLen = written; - return 0; -} - -template -int fromMbcs(const void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, Target* target, uint32_t targetMax, uint32_t* targetLen, Encode encode) { - if (!context) return CES_ERROR_INVALID_PARAMETER; - const void* profile = contextProfile(context); - if (!isMbcs(profile)) return CES_ERROR_INVALID_PARAMETER; - if (!mbcs) return CES_ERROR_INVALID_SRC_BUFFER; - uint32_t read = 0; - uint32_t written = 0; - while (read < mbcsMax && mbcs[read] != 0) { - uint32_t length = 0; - uint32_t code = 0; - if (const int result = decodeMbcs(profile, mbcs + read, mbcsMax - read, length, code); result != 0) return result; + const uint32_t available = sourceMax == 0 ? UINT32_MAX - read : sourceMax - read; + if ((result = decode(profile, source + read, available, length, code)) != 0) break; Target encoded[4]; - const uint32_t size = encode(code, encoded); - if (size == 0) return CES_ERROR_UNASSIGNED_CODE; - if (target) { - if (written + size > targetMax) return CES_ERROR_DST_BUFFER_END; + uint32_t size = 0; + if ((result = encode(profile, code, encoded, size)) != 0) break; + if (!counting) { + if (written + size >= targetMax) { + result = CES_ERROR_DST_BUFFER_END; + break; + } std::memcpy(target + written, encoded, size * sizeof(Target)); } read += length; written += size; } - if (mbcsLen) *mbcsLen = read; + if (!counting && written < targetMax) target[written] = 0; + if (sourceLen) *sourceLen = read; if (targetLen) *targetLen = written; - return 0; + return result; +} + +template +int rejected(int result, uint32_t length, uint32_t* sourceSize, Unit* character, uint32_t* produced) { + if (length == 0) return result; + if (sourceSize) *sourceSize = length; + if (character) *character = 0; + if (produced) *produced = 0; + return result; } int mbcsToUtf8(bool accepted, const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint8_t* utf8, uint32_t utf8Max, uint32_t* utf8Len) { @@ -832,7 +944,7 @@ int mbcsToUtf8(bool accepted, const void* profile, const uint8_t* mbcs, uint32_t if (!utf8) return CES_ERROR_INVALID_DST_BUFFER; uint32_t consumed = 0; uint32_t code = 0; - if (const int result = decodeMbcs(profile, mbcs, mbcsMax, consumed, code); result != 0) return result; + if (const int result = decodeMbcs(profile, mbcs, mbcsMax, consumed, code); result != 0) return rejected(result, consumed, mbcsLen, nullptr, utf8Len); uint8_t encoded[4]; const uint32_t produced = encodeUtf8(code, encoded); if (utf8Max < produced) return CES_ERROR_DST_BUFFER_END; @@ -862,6 +974,11 @@ bool isScalar(uint32_t code) { return code <= 0x10FFFF && (code < 0xD800 || code > 0xDFFF); } +int scalarError(uint32_t code) { + if (code > 0x10FFFF) return CES_ERROR_INVALID_ENCODE; + return code >= 0xD800 && code <= 0xDFFF ? CES_ERROR_ILLEGAL_CODE : 0; +} + template int decodeUtf16(const uint16_t* utf16, uint32_t utf16max, uint32_t& length, uint32_t& code) { code = loadUnit(utf16); @@ -885,10 +1002,11 @@ int decodeProfiled(bool (*matches)(const void*), const void* profile, const uint return decodeMbcs(profile, source, sourceMax, length, code); } + int profiledToUcs2(bool (*matches)(const void*), const void* profile, const uint8_t* source, uint32_t sourceMax, uint32_t* sourceSize, uint16_t* ucs2) { uint32_t length = 0; uint32_t code = 0; - if (const int result = decodeProfiled(matches, profile, source, sourceMax, ucs2, length, code); result != 0) return result; + if (const int result = decodeProfiled(matches, profile, source, sourceMax, ucs2, length, code); result != 0) return rejected(result, length, sourceSize, ucs2, nullptr); if (code > 0xFFFF) return CES_ERROR_UNASSIGNED_CODE; *ucs2 = static_cast(code); if (sourceSize) *sourceSize = length; @@ -898,7 +1016,7 @@ int profiledToUcs2(bool (*matches)(const void*), const void* profile, const uint int profiledToUtf8(bool (*matches)(const void*), const void* profile, const uint8_t* source, uint32_t sourceMax, uint32_t* sourceSize, uint8_t* utf8, uint32_t utf8max, uint32_t* utf8Len) { uint32_t length = 0; uint32_t code = 0; - if (const int result = decodeProfiled(matches, profile, source, sourceMax, utf8, length, code); result != 0) return result; + if (const int result = decodeProfiled(matches, profile, source, sourceMax, utf8, length, code); result != 0) return rejected(result, length, sourceSize, nullptr, utf8Len); uint8_t encoded[4]; const uint32_t produced = encodeUtf8(code, encoded); if (utf8max < produced) return CES_ERROR_DST_BUFFER_END; @@ -912,7 +1030,7 @@ template int profiledToUtf16(bool (*matches)(const void*), const void* profile, const uint8_t* source, uint32_t sourceMax, uint32_t* sourceSize, uint16_t* utf16, uint32_t utf16max, uint32_t* utf16Len) { uint32_t length = 0; uint32_t code = 0; - if (const int result = decodeProfiled(matches, profile, source, sourceMax, utf16, length, code); result != 0) return result; + if (const int result = decodeProfiled(matches, profile, source, sourceMax, utf16, length, code); result != 0) return rejected(result, length, sourceSize, nullptr, utf16Len); const uint32_t units = code < 0x10000 ? 1 : 2; if (utf16max < units) return CES_ERROR_DST_BUFFER_END; if (units == 1) { @@ -930,7 +1048,7 @@ template int profiledToUtf32(bool (*matches)(const void*), const void* profile, const uint8_t* source, uint32_t sourceMax, uint32_t* sourceSize, uint32_t* utf32) { uint32_t length = 0; uint32_t code = 0; - if (const int result = decodeProfiled(matches, profile, source, sourceMax, utf32, length, code); result != 0) return result; + if (const int result = decodeProfiled(matches, profile, source, sourceMax, utf32, length, code); result != 0) return rejected(result, length, sourceSize, utf32, nullptr); storeUnit(utf32, code); if (sourceSize) *sourceSize = length; return 0; @@ -949,8 +1067,14 @@ int encodeProfiled(const void* profile, uint32_t code, uint8_t* target, uint32_t int scalarToProfiled(bool (*matches)(const void*), uint32_t code, const void* profile, uint8_t* target, uint32_t targetMax, uint32_t* targetSize) { if (!matches(profile)) return CES_ERROR_INVALID_PARAMETER; if (!target) return CES_ERROR_INVALID_DST_BUFFER; - if (!isScalar(code)) return CES_ERROR_INVALID_ENCODE; - return encodeProfiled(profile, code, target, targetMax, targetSize); + int result = scalarError(code); + uint8_t encoded[4]; + uint32_t size = 0; + if (result == 0) result = encodeMbcs(profile, code, encoded, size); + if (result == 0 && targetMax < size) result = CES_ERROR_DST_BUFFER_END; + if (result == 0) std::memcpy(target, encoded, size); + if (targetSize) *targetSize = result == 0 || result == CES_ERROR_DST_BUFFER_END ? size : 0; + return result; } template @@ -985,11 +1109,21 @@ uint32_t encodeUtf32(uint32_t code, uint32_t* utf32) { return 1; } +template +int unitSource(const void*, const Unit* source, uint32_t sourceMax, uint32_t& length, uint32_t& code) { + return Decode(source, sourceMax, length, code); +} + +template +int unitTarget(const void*, uint32_t code, Unit* target, uint32_t& length) { + length = Encode(code, target); + return length != 0 ? 0 : CES_ERROR_UNASSIGNED_CODE; +} + int sbcToUnit(const uint8_t* profile, uint8_t sbc, const void* target, uint32_t& code) { if (!isSbc(profile)) return CES_ERROR_INVALID_PARAMETER; if (!target) return CES_ERROR_INVALID_DST_BUFFER; - code = sbcToUnicode(profile, sbc); - return 0; + return sbcToUnicode(profile, sbc, code) ? 0 : CES_ERROR_UNASSIGNED_CODE; } template @@ -1110,6 +1244,10 @@ const uint8_t* APS5_VABI sceCesRefersUcsProfileCp850(void) { return singleByteProfile(SingleByte::Cp850); } +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp858(void) { + return singleByteProfile(SingleByte::Cp858); +} + const uint8_t* APS5_VABI sceCesRefersUcsProfileCp852(void) { return singleByteProfile(SingleByte::Cp852); } @@ -1175,7 +1313,7 @@ void* APS5_VABI sceCesUcsProfileInitSJis1997X0208(void* sheet) { } int APS5_VABI sceCesMbcsUcsContextInit(void* context, const void* profile) { - if (!context) return CES_ERROR_INVALID_PARAMETER; + if (!context || !profile) return CES_ERROR_INVALID_PARAMETER; std::memcpy(context, &profile, sizeof(profile)); return 0; } @@ -1230,94 +1368,103 @@ void* APS5_VABI sceCesUcsProfileInitBig5Cp950(void* sheet) { } int APS5_VABI sceCesUtf8StrToMbcsStr(void* context, const uint8_t* utf8, uint32_t utf8max, uint32_t* utf8Len, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen) { - if (!mbcs) return CES_ERROR_INVALID_DST_BUFFER; - return toMbcs(context, utf8, utf8max, utf8Len, mbcs, mbcsMax, mbcsLen, decodeUtf8); + return convertString(context, utf8, utf8max, utf8Len, mbcs, mbcsMax, mbcsLen, false, unitSource, encodeMbcs); } int APS5_VABI sceCesUtf8StrGetMbcsLen(void* context, const uint8_t* utf8, uint32_t utf8max, uint32_t* utf8Len, uint32_t* mbcsLen) { - return toMbcs(context, utf8, utf8max, utf8Len, static_cast(nullptr), 0, mbcsLen, decodeUtf8); + return convertString(context, utf8, utf8max, utf8Len, static_cast(nullptr), 0, mbcsLen, true, unitSource, encodeMbcs); } int APS5_VABI sceCesUcs2StrToMbcsStr(void* context, const uint16_t* ucs2, uint32_t ucs2max, uint32_t* ucs2Len, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen) { - if (!mbcs) return CES_ERROR_INVALID_DST_BUFFER; - return toMbcs(context, ucs2, ucs2max, ucs2Len, mbcs, mbcsMax, mbcsLen, decodeUcs2); + return convertString(context, ucs2, ucs2max, ucs2Len, mbcs, mbcsMax, mbcsLen, false, unitSource, encodeMbcs); } int APS5_VABI sceCesUcs2StrGetMbcsLen(void* context, const uint16_t* ucs2, uint32_t ucs2max, uint32_t* ucs2Len, uint32_t* mbcsLen) { - return toMbcs(context, ucs2, ucs2max, ucs2Len, static_cast(nullptr), 0, mbcsLen, decodeUcs2); + return convertString(context, ucs2, ucs2max, ucs2Len, static_cast(nullptr), 0, mbcsLen, true, unitSource, encodeMbcs); } int APS5_VABI sceCesMbcsStrToUtf8Str(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint8_t* utf8, uint32_t utf8max, uint32_t* utf8Len) { - if (!utf8) return CES_ERROR_INVALID_DST_BUFFER; - return fromMbcs(context, mbcs, mbcsMax, mbcsLen, utf8, utf8max, utf8Len, encodeUtf8); + return convertString(context, mbcs, mbcsMax, mbcsLen, utf8, utf8max, utf8Len, false, decodeMbcs, unitTarget); } int APS5_VABI sceCesMbcsStrGetUtf8Len(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint32_t* utf8Len) { - return fromMbcs(context, mbcs, mbcsMax, mbcsLen, static_cast(nullptr), 0, utf8Len, encodeUtf8); + return convertString(context, mbcs, mbcsMax, mbcsLen, static_cast(nullptr), 0, utf8Len, true, decodeMbcs, unitTarget); } int APS5_VABI sceCesMbcsStrToUcs2Str(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint16_t* ucs2, uint32_t ucs2max, uint32_t* ucs2Len) { - if (!ucs2) return CES_ERROR_INVALID_DST_BUFFER; - return fromMbcs(context, mbcs, mbcsMax, mbcsLen, ucs2, ucs2max, ucs2Len, encodeUcs2); + return convertString(context, mbcs, mbcsMax, mbcsLen, ucs2, ucs2max, ucs2Len, false, decodeMbcs, unitTarget); } int APS5_VABI sceCesMbcsStrGetUcs2Len(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint32_t* ucs2Len) { - return fromMbcs(context, mbcs, mbcsMax, mbcsLen, static_cast(nullptr), 0, ucs2Len, encodeUcs2); + return convertString(context, mbcs, mbcsMax, mbcsLen, static_cast(nullptr), 0, ucs2Len, true, decodeMbcs, unitTarget); } int APS5_VABI sceCesEucJpToUcs2(const void* profile, const uint8_t* euc, uint32_t eucMax, uint32_t* eucSize, uint16_t* ucs2) { + if (eucSize) *eucSize = 0; + if (ucs2) *ucs2 = 0; + if (!profile) return CES_ERROR_INVALID_PROFILE; if (!isEucJp(profile)) return CES_ERROR_INVALID_PARAMETER; - if (!euc) return CES_ERROR_INVALID_SRC_BUFFER; - if (eucMax == 0) return CES_ERROR_SRC_BUFFER_END; - if (!ucs2) return CES_ERROR_INVALID_DST_BUFFER; + if (!euc || eucMax == 0) return CES_ERROR_INVALID_SRC_BUFFER; uint32_t length = 0; uint32_t code = 0; - if (const int result = decodeMbcs(profile, euc, eucMax, length, code); result != 0) return result; - *ucs2 = static_cast(code); + const int decoded = decodeEucJp(profile, euc, eucMax, length, code); if (eucSize) *eucSize = length; + if (decoded != 0) return decoded; + if (!ucs2) return CES_ERROR_INVALID_DST_BUFFER; + *ucs2 = static_cast(code); return 0; } int APS5_VABI sceCesSbcToUtf8(const uint8_t* profile, uint8_t sbc, uint8_t* utf8, uint32_t utf8max, uint32_t* utf8_len) { + if (utf8_len) *utf8_len = 0; + if (!profile) return CES_ERROR_INVALID_PROFILE; if (!isSbc(profile)) return CES_ERROR_INVALID_PARAMETER; - if (!utf8) return CES_ERROR_INVALID_DST_BUFFER; - + uint32_t code = 0; + if (!sbcToUnicode(profile, sbc, code)) return CES_ERROR_UNASSIGNED_CODE; uint8_t encoded[4]; - const uint32_t length = encodeUtf8(sbcToUnicode(profile, sbc), encoded); + const uint32_t length = encodeUtf8(code, encoded); + if (utf8_len) *utf8_len = length; + if (!utf8) return CES_ERROR_INVALID_DST_BUFFER; if (utf8max < length) return CES_ERROR_DST_BUFFER_END; for (uint32_t i = 0; i < length; ++i) utf8[i] = encoded[i]; - if (utf8_len) *utf8_len = length; return 0; } int APS5_VABI sceCesUtf8ToSbc(const uint8_t* utf8, uint32_t utf8max, uint32_t* utf8_len, const uint8_t* profile, uint8_t* sbc) { - if (!isSbc(profile)) return CES_ERROR_INVALID_PARAMETER; - if (!utf8) return CES_ERROR_INVALID_SRC_BUFFER; - if (utf8max == 0) return CES_ERROR_SRC_BUFFER_END; - if (!sbc) return CES_ERROR_INVALID_DST_BUFFER; - + if (utf8_len) *utf8_len = 0; + if (sbc) *sbc = 0; + if (!utf8 || utf8max == 0) return CES_ERROR_INVALID_SRC_BUFFER; uint32_t length = 0; uint32_t code = 0; - if (const int result = decodeUtf8(utf8, utf8max, length, code); result != 0) return result; - if (!unicodeToSbc(profile, code, *sbc)) return CES_ERROR_UNASSIGNED_CODE; + const int decoded = decodeScalarUtf8(utf8, utf8max, length, code); if (utf8_len) *utf8_len = length; + if (decoded != 0) return decoded; + if (!profile) return CES_ERROR_INVALID_PROFILE; + if (!isSbc(profile)) return CES_ERROR_INVALID_PARAMETER; + uint8_t value = 0; + if (!unicodeToSbc(profile, code, value)) return CES_ERROR_UNASSIGNED_CODE; + if (!sbc) return CES_ERROR_INVALID_DST_BUFFER; + *sbc = value; return 0; } int APS5_VABI sceCesEucJpToUtf8(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucLength, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length) { + if (eucLength) *eucLength = 0; + if (utf8Length) *utf8Length = 0; + if (!profile) return CES_ERROR_INVALID_PROFILE; if (!isEucJp(profile)) return CES_ERROR_INVALID_PARAMETER; - if (!eucBuffer) return CES_ERROR_INVALID_SRC_BUFFER; - if (eucMax == 0) return CES_ERROR_SRC_BUFFER_END; - if (!utf8Buffer) return CES_ERROR_INVALID_DST_BUFFER; + if (!eucBuffer || eucMax == 0) return CES_ERROR_INVALID_SRC_BUFFER; uint32_t consumed = 0; uint32_t code = 0; - if (const int result = decodeMbcs(profile, eucBuffer, eucMax, consumed, code); result != 0) return result; + const int decoded = decodeEucJp(profile, eucBuffer, eucMax, consumed, code); + if (eucLength) *eucLength = consumed; + if (decoded != 0) return decoded; uint8_t encoded[4]; const uint32_t produced = encodeUtf8(code, encoded); + if (utf8Length) *utf8Length = produced; + if (!utf8Buffer) return CES_ERROR_INVALID_DST_BUFFER; if (utf8Max < produced) return CES_ERROR_DST_BUFFER_END; std::memcpy(utf8Buffer, encoded, produced); - if (eucLength) *eucLength = consumed; - if (utf8Length) *utf8Length = produced; return 0; } @@ -1394,7 +1541,7 @@ int APS5_VABI sceCesUcs2ToSJis(uint16_t ucs2, const void* profile, uint8_t* sjis } int APS5_VABI sceCesUtf8ToSJis(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length, const void* profile, uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize) { - return bufferToProfiled(isSjis, utf8Buffer, utf8Max, utf8Length, profile, sjisBuffer, sjisMax, sjisSize, decodeUtf8); + return bufferToProfiled(isSjis, utf8Buffer, utf8Max, utf8Length, profile, sjisBuffer, sjisMax, sjisSize, decodeScalarUtf8); } int APS5_VABI sceCesUtf16ToSJis(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize) { @@ -1426,7 +1573,7 @@ int APS5_VABI sceCesUcs2ToEucJp(uint16_t ucs2, const void* profile, uint8_t* euc } int APS5_VABI sceCesUtf8ToEucJp(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { - return bufferToProfiled(isEucJp, utf8Buffer, utf8Max, utf8Length, profile, eucBuffer, eucMax, eucSize, decodeUtf8); + return bufferToProfiled(isEucJp, utf8Buffer, utf8Max, utf8Length, profile, eucBuffer, eucMax, eucSize, decodeScalarUtf8); } int APS5_VABI sceCesUtf16ToEucJp(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { @@ -1567,6 +1714,38 @@ int APS5_VABI sceCesMbcToUtf32le(const void* profile, const uint8_t* mbcBuffer, return profiledToUtf32(isMbcs, profile, mbcBuffer, mbcMax, mbcLen, utf32); } +int APS5_VABI sceCesUcs2ToMbc(uint16_t ucs2, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen) { + return scalarToProfiled(isMbcs, ucs2, profile, mbcBuffer, mbcMax, mbcLen); +} + +int APS5_VABI sceCesUtf8ToMbc(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Len, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen) { + return bufferToProfiled(isMbcs, utf8Buffer, utf8Max, utf8Len, profile, mbcBuffer, mbcMax, mbcLen, decodeScalarUtf8); +} + +int APS5_VABI sceCesUtf16ToMbc(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Len, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen) { + return bufferToProfiled(isMbcs, utf16Buffer, utf16Max, utf16Len, profile, mbcBuffer, mbcMax, mbcLen, decodeUtf16); +} + +int APS5_VABI sceCesUtf16beToMbc(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Len, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen) { + return bufferToProfiled(isMbcs, utf16Buffer, utf16Max, utf16Len, profile, mbcBuffer, mbcMax, mbcLen, decodeUtf16); +} + +int APS5_VABI sceCesUtf16leToMbc(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Len, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen) { + return bufferToProfiled(isMbcs, utf16Buffer, utf16Max, utf16Len, profile, mbcBuffer, mbcMax, mbcLen, decodeUtf16); +} + +int APS5_VABI sceCesUtf32ToMbc(uint32_t utf32, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen) { + return scalarToProfiled(isMbcs, utf32, profile, mbcBuffer, mbcMax, mbcLen); +} + +int APS5_VABI sceCesUtf32beToMbc(const uint32_t* utf32, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen) { + return utf32ToProfiled(isMbcs, utf32, profile, mbcBuffer, mbcMax, mbcLen); +} + +int APS5_VABI sceCesUtf32leToMbc(const uint32_t* utf32, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen) { + return utf32ToProfiled(isMbcs, utf32, profile, mbcBuffer, mbcMax, mbcLen); +} + int APS5_VABI sceCesSbcToUcs2(const uint8_t* profile, uint8_t sbc, uint16_t* ucs2) { uint32_t code = 0; if (const int result = sbcToUnit(profile, sbc, ucs2, code); result != 0) return result; @@ -1627,39 +1806,35 @@ int APS5_VABI sceCesUtf32leToSbc(const uint32_t* utf32, const uint8_t* profile, } int APS5_VABI sceCesUtf16StrToMbcsStr(void* context, const uint16_t* utf16, uint32_t utf16max, uint32_t* utf16Len, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen) { - if (!mbcs) return CES_ERROR_INVALID_DST_BUFFER; - return toMbcs(context, utf16, utf16max, utf16Len, mbcs, mbcsMax, mbcsLen, decodeUtf16); + return convertString(context, utf16, utf16max, utf16Len, mbcs, mbcsMax, mbcsLen, false, unitSource, uint16_t>, encodeMbcs); } int APS5_VABI sceCesUtf16StrGetMbcsLen(void* context, const uint16_t* utf16, uint32_t utf16max, uint32_t* utf16Len, uint32_t* mbcsLen) { - return toMbcs(context, utf16, utf16max, utf16Len, static_cast(nullptr), 0, mbcsLen, decodeUtf16); + return convertString(context, utf16, utf16max, utf16Len, static_cast(nullptr), 0, mbcsLen, true, unitSource, uint16_t>, encodeMbcs); } int APS5_VABI sceCesMbcsStrToUtf16Str(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint16_t* utf16, uint32_t utf16max, uint32_t* utf16Len) { - if (!utf16) return CES_ERROR_INVALID_DST_BUFFER; - return fromMbcs(context, mbcs, mbcsMax, mbcsLen, utf16, utf16max, utf16Len, encodeUtf16); + return convertString(context, mbcs, mbcsMax, mbcsLen, utf16, utf16max, utf16Len, false, decodeMbcs, unitTarget); } int APS5_VABI sceCesMbcsStrGetUtf16Len(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint32_t* utf16Len) { - return fromMbcs(context, mbcs, mbcsMax, mbcsLen, static_cast(nullptr), 0, utf16Len, encodeUtf16); + return convertString(context, mbcs, mbcsMax, mbcsLen, static_cast(nullptr), 0, utf16Len, true, decodeMbcs, unitTarget); } int APS5_VABI sceCesUtf32StrToMbcsStr(void* context, const uint32_t* utf32, uint32_t utf32max, uint32_t* utf32Len, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen) { - if (!mbcs) return CES_ERROR_INVALID_DST_BUFFER; - return toMbcs(context, utf32, utf32max, utf32Len, mbcs, mbcsMax, mbcsLen, decodeUtf32); + return convertString(context, utf32, utf32max, utf32Len, mbcs, mbcsMax, mbcsLen, false, unitSource, encodeMbcs); } int APS5_VABI sceCesUtf32StrGetMbcsLen(void* context, const uint32_t* utf32, uint32_t utf32max, uint32_t* utf32Len, uint32_t* mbcsLen) { - return toMbcs(context, utf32, utf32max, utf32Len, static_cast(nullptr), 0, mbcsLen, decodeUtf32); + return convertString(context, utf32, utf32max, utf32Len, static_cast(nullptr), 0, mbcsLen, true, unitSource, encodeMbcs); } int APS5_VABI sceCesMbcsStrToUtf32Str(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint32_t* utf32, uint32_t utf32max, uint32_t* utf32Len) { - if (!utf32) return CES_ERROR_INVALID_DST_BUFFER; - return fromMbcs(context, mbcs, mbcsMax, mbcsLen, utf32, utf32max, utf32Len, encodeUtf32); + return convertString(context, mbcs, mbcsMax, mbcsLen, utf32, utf32max, utf32Len, false, decodeMbcs, unitTarget); } int APS5_VABI sceCesMbcsStrGetUtf32Len(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint32_t* utf32Len) { - return fromMbcs(context, mbcs, mbcsMax, mbcsLen, static_cast(nullptr), 0, utf32Len, encodeUtf32); + return convertString(context, mbcs, mbcsMax, mbcsLen, static_cast(nullptr), 0, utf32Len, true, decodeMbcs, unitTarget); } void* APS5_VABI sceCesUcsProfileInitEucCnGb2312(void* sheet) { @@ -1723,7 +1898,7 @@ int APS5_VABI sceCesUcs2ToEucCn(uint16_t ucs2, const void* profile, uint8_t* euc } int APS5_VABI sceCesUtf8ToEucCn(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { - return bufferToProfiled(isEucCn, utf8Buffer, utf8Max, utf8Length, profile, eucBuffer, eucMax, eucSize, decodeUtf8); + return bufferToProfiled(isEucCn, utf8Buffer, utf8Max, utf8Length, profile, eucBuffer, eucMax, eucSize, decodeScalarUtf8); } int APS5_VABI sceCesUtf16ToEucCn(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { @@ -1787,7 +1962,7 @@ int APS5_VABI sceCesUcs2ToGb(uint16_t ucs2, const void* profile, uint8_t* gbBuff } int APS5_VABI sceCesUtf8ToGb(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { - return bufferToProfiled(isGb, utf8Buffer, utf8Max, utf8Length, profile, gbBuffer, gbMax, gbSize, decodeUtf8); + return bufferToProfiled(isGb, utf8Buffer, utf8Max, utf8Length, profile, gbBuffer, gbMax, gbSize, decodeScalarUtf8); } int APS5_VABI sceCesUtf16ToGb(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { @@ -1903,7 +2078,7 @@ int APS5_VABI sceCesUcs2ToEucKr(uint16_t ucs2, const void* profile, uint8_t* euc } int APS5_VABI sceCesUtf8ToEucKr(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { - return bufferToProfiled(isEucKr, utf8Buffer, utf8Max, utf8Length, profile, eucBuffer, eucMax, eucSize, decodeUtf8); + return bufferToProfiled(isEucKr, utf8Buffer, utf8Max, utf8Length, profile, eucBuffer, eucMax, eucSize, decodeScalarUtf8); } int APS5_VABI sceCesUtf16ToEucKr(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { @@ -1935,7 +2110,7 @@ int APS5_VABI sceCesUcs2ToUhc(uint16_t ucs2, const void* profile, uint8_t* uhcBu } int APS5_VABI sceCesUtf8ToUhc(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length, const void* profile, uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize) { - return bufferToProfiled(isUhc, utf8Buffer, utf8Max, utf8Length, profile, uhcBuffer, uhcMax, uhcSize, decodeUtf8); + return bufferToProfiled(isUhc, utf8Buffer, utf8Max, utf8Length, profile, uhcBuffer, uhcMax, uhcSize, decodeScalarUtf8); } int APS5_VABI sceCesUtf16ToUhc(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize) { @@ -1967,7 +2142,7 @@ int APS5_VABI sceCesUcs2ToBig5(uint16_t ucs2, const void* profile, uint8_t* big5 } int APS5_VABI sceCesUtf8ToBig5(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length, const void* profile, uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size) { - return bufferToProfiled(isBig5, utf8Buffer, utf8Max, utf8Length, profile, big5Buffer, big5Max, big5Size, decodeUtf8); + return bufferToProfiled(isBig5, utf8Buffer, utf8Max, utf8Length, profile, big5Buffer, big5Max, big5Size, decodeScalarUtf8); } int APS5_VABI sceCesUtf16ToBig5(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size) { diff --git a/core/libs/prx/libSceCesCs/PendingExports.cpp b/core/libs/prx/libSceCesCs/PendingExports.cpp index 849f73b092..6c2fffa901 100644 --- a/core/libs/prx/libSceCesCs/PendingExports.cpp +++ b/core/libs/prx/libSceCesCs/PendingExports.cpp @@ -143,11 +143,6 @@ int APS5_VABI sceCesRefersUcsProfileCp857(void) { return 0; } -int APS5_VABI sceCesRefersUcsProfileCp858(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesRefersUcsProfileCp864(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -248,11 +243,6 @@ int APS5_VABI sceCesUcs2StrToIso2022Str(void) { return 0; } -int APS5_VABI sceCesUcs2ToMbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcsProfileInitEucJis2004(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -298,21 +288,6 @@ int APS5_VABI sceCesUtf16StrToIso2022Str(void) { return 0; } -int APS5_VABI sceCesUtf16ToMbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUtf16beToMbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUtf16leToMbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32StrGetIso2022Len(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -323,21 +298,6 @@ int APS5_VABI sceCesUtf32StrToIso2022Str(void) { return 0; } -int APS5_VABI sceCesUtf32ToMbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUtf32beToMbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUtf32leToMbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf8StrGetIso2022Len(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -348,9 +308,4 @@ int APS5_VABI sceCesUtf8StrToIso2022Str(void) { return 0; } -int APS5_VABI sceCesUtf8ToMbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceCesCs/SingleByte.hpp b/core/libs/prx/libSceCesCs/SingleByte.hpp index ebe46104cd..6f24ada575 100644 --- a/core/libs/prx/libSceCesCs/SingleByte.hpp +++ b/core/libs/prx/libSceCesCs/SingleByte.hpp @@ -33,6 +33,7 @@ enum Table : std::size_t { Cp1254, Cp1256, Cp1258, + Cp858, Count, }; @@ -307,6 +308,16 @@ inline constexpr uint16_t HIGH[Count][128] = { 0x00E0, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x00E5, 0x00E6, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x0301, 0x00ED, 0x00EE, 0x00EF, 0x0111, 0x00F1, 0x0323, 0x00F3, 0x00F4, 0x01A1, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x01B0, 0x20AB, 0x00FF, }, + { + 0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7, 0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x00EC, 0x00C4, 0x00C5, + 0x00C9, 0x00E6, 0x00C6, 0x00F4, 0x00F6, 0x00F2, 0x00FB, 0x00F9, 0x00FF, 0x00D6, 0x00DC, 0x00F8, 0x00A3, 0x00D8, 0x00D7, 0x0192, + 0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x00F1, 0x00D1, 0x00AA, 0x00BA, 0x00BF, 0x00AE, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x00C1, 0x00C2, 0x00C0, 0x00A9, 0x2563, 0x2551, 0x2557, 0x255D, 0x00A2, 0x00A5, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x00E3, 0x00C3, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x00A4, + 0x00F0, 0x00D0, 0x00CA, 0x00CB, 0x00C8, 0x20AC, 0x00CD, 0x00CE, 0x00CF, 0x2518, 0x250C, 0x2588, 0x2584, 0x00A6, 0x00CC, 0x2580, + 0x00D3, 0x00DF, 0x00D4, 0x00D2, 0x00F5, 0x00D5, 0x00B5, 0x00FE, 0x00DE, 0x00DA, 0x00DB, 0x00D9, 0x00FD, 0x00DD, 0x00AF, 0x00B4, + 0x00AD, 0x00B1, 0x2017, 0x00BE, 0x00B6, 0x00A7, 0x00F7, 0x00B8, 0x00B0, 0x00A8, 0x00B7, 0x00B9, 0x00B3, 0x00B2, 0x25A0, 0x00A0, + }, }; } diff --git a/core/libs/prx/libSceCesCs/include/Api.hpp b/core/libs/prx/libSceCesCs/include/Api.hpp index 4ad8a366f1..0dbb5beac0 100644 --- a/core/libs/prx/libSceCesCs/include/Api.hpp +++ b/core/libs/prx/libSceCesCs/include/Api.hpp @@ -34,6 +34,7 @@ const uint8_t* APS5_VABI sceCesRefersUcsProfileKoi8U(void); const uint8_t* APS5_VABI sceCesRefersUcsProfileCp1254(void); const uint8_t* APS5_VABI sceCesRefersUcsProfileCp1256(void); const uint8_t* APS5_VABI sceCesRefersUcsProfileCp1258(void); +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp858(void); void* APS5_VABI sceCesUcsProfileInitSJis1997Cp932(void* sheet); void* APS5_VABI sceCesUcsProfileInitSJis1997X0208(void* sheet); int APS5_VABI sceCesMbcsUcsContextInit(void* context, const void* profile); @@ -54,8 +55,8 @@ int APS5_VABI sceCesMbcsStrGetUtf8Len(void* context, const uint8_t* mbcs, uint32 int APS5_VABI sceCesMbcsStrToUcs2Str(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint16_t* ucs2, uint32_t ucs2max, uint32_t* ucs2Len); int APS5_VABI sceCesMbcsStrGetUcs2Len(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint32_t* ucs2Len); int APS5_VABI sceCesEucJpToUcs2(const void* profile, const uint8_t* euc, uint32_t eucMax, uint32_t* eucSize, uint16_t* ucs2); -int APS5_VABI sceCesSbcToUtf8(const uint8_t* profile, uint8_t sbc, uint8_t* utf8, uint32_t utf8max, uint32_t* utf8Len); -int APS5_VABI sceCesUtf8ToSbc(const uint8_t* utf8, uint32_t utf8max, uint32_t* utf8Len, const uint8_t* profile, uint8_t* sbc); +int APS5_VABI sceCesSbcToUtf8(const uint8_t* profile, uint8_t sbc, uint8_t* utf8, uint32_t utf8max, uint32_t* utf8_len); +int APS5_VABI sceCesUtf8ToSbc(const uint8_t* utf8, uint32_t utf8max, uint32_t* utf8_len, const uint8_t* profile, uint8_t* sbc); int APS5_VABI sceCesEucJpToUtf8(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucLength, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length); int APS5_VABI sceCesEucKrToUtf8(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucLength, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length); int APS5_VABI sceCesBig5ToUtf8(const void* profile, const uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Length, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length); @@ -235,7 +236,6 @@ int APS5_VABI sceCesRefersUcsProfileCp1253(void); int APS5_VABI sceCesRefersUcsProfileCp1255(void); int APS5_VABI sceCesRefersUcsProfileCp1257(void); int APS5_VABI sceCesRefersUcsProfileCp857(void); -int APS5_VABI sceCesRefersUcsProfileCp858(void); int APS5_VABI sceCesRefersUcsProfileCp864(void); int APS5_VABI sceCesRefersUcsProfileCp869(void); int APS5_VABI sceCesRefersUcsProfileCp874(void); @@ -256,7 +256,7 @@ int APS5_VABI sceCesRefersUcsProfileKsX1003Tilde0x7e(void); int APS5_VABI sceCesSJisUcsProfileSetSbcs(void); int APS5_VABI sceCesUcs2StrGetIso2022Len(void); int APS5_VABI sceCesUcs2StrToIso2022Str(void); -int APS5_VABI sceCesUcs2ToMbc(void); +int APS5_VABI sceCesUcs2ToMbc(uint16_t ucs2, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen); int APS5_VABI sceCesUcsProfileInitEucJis2004(void); int APS5_VABI sceCesUcsProfileInitIso2022JpCp50221(void); int APS5_VABI sceCesUcsProfileInitIso2022JpCp50222(void); @@ -266,17 +266,17 @@ int APS5_VABI sceCesUcsToJiscs(void); int APS5_VABI sceCesUhcUcsProfileSetSbcs(void); int APS5_VABI sceCesUtf16StrGetIso2022Len(void); int APS5_VABI sceCesUtf16StrToIso2022Str(void); -int APS5_VABI sceCesUtf16ToMbc(void); -int APS5_VABI sceCesUtf16beToMbc(void); -int APS5_VABI sceCesUtf16leToMbc(void); +int APS5_VABI sceCesUtf16ToMbc(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Len, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen); +int APS5_VABI sceCesUtf16beToMbc(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Len, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen); +int APS5_VABI sceCesUtf16leToMbc(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Len, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen); int APS5_VABI sceCesUtf32StrGetIso2022Len(void); int APS5_VABI sceCesUtf32StrToIso2022Str(void); -int APS5_VABI sceCesUtf32ToMbc(void); -int APS5_VABI sceCesUtf32beToMbc(void); -int APS5_VABI sceCesUtf32leToMbc(void); +int APS5_VABI sceCesUtf32ToMbc(uint32_t utf32, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen); +int APS5_VABI sceCesUtf32beToMbc(const uint32_t* utf32, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen); +int APS5_VABI sceCesUtf32leToMbc(const uint32_t* utf32, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen); int APS5_VABI sceCesUtf8StrGetIso2022Len(void); int APS5_VABI sceCesUtf8StrToIso2022Str(void); -int APS5_VABI sceCesUtf8ToMbc(void); +int APS5_VABI sceCesUtf8ToMbc(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Len, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen); int APS5_VABI sceCesUcs2ToUtf8(uint16_t source, uint8_t* target, uint32_t capacity, uint32_t* targetLength); int APS5_VABI sceCesUtf8ToUcs2(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint16_t* target); int APS5_VABI sceCesUtf8ToUtf32(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint32_t* target); diff --git a/core/libs/prx/libSceComposite/Export.cpp b/core/libs/prx/libSceComposite/Export.cpp index a2e79ae3ed..96949e8e2f 100644 --- a/core/libs/prx/libSceComposite/Export.cpp +++ b/core/libs/prx/libSceComposite/Export.cpp @@ -27,13 +27,11 @@ int APS5_VABI sceCompositorCommandGpuPerfEnd() { } int APS5_VABI sceCompositorCreateIndirectRenderTarget() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80d40006); } int APS5_VABI sceCompositorDeleteIndirectRenderTarget() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80d40006); } int APS5_VABI sceCompositorFlush() { @@ -262,17 +260,14 @@ int APS5_VABI sceCompositorSetGnmContextCommand() { } int APS5_VABI sceCompositorSetIndirectCanvasCommand() { - NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceCompositorSetIndirectCompositionCommand() { - NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceCompositorSetIndirectRenderTargetConfigCommand() { - NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceContentExport/Export.cpp b/core/libs/prx/libSceContentExport/Export.cpp index 975349f1e5..5ec29d9900 100644 --- a/core/libs/prx/libSceContentExport/Export.cpp +++ b/core/libs/prx/libSceContentExport/Export.cpp @@ -130,7 +130,8 @@ int APS5_VABI sceContentExportStart(void) { int APS5_VABI sceContentExportGetProgress(std::int32_t expId) { std::lock_guard lock(g_sessionMutex); RequireSession(__func__, expId); - throw std::runtime_error(std::string(__func__) + ": progress requires the calling title SDK compatibility mode"); + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceContentExportCancel(std::int32_t expId) { @@ -142,7 +143,8 @@ int APS5_VABI sceContentExportCancel(std::int32_t expId) { int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const ContentExportParam* param, const ContentExportCallbackParam* contentCallbackParam, const char* thumbnailType, const ContentExportCallbackParam* thumbnailCallbackParam, char* path, std::size_t pathBufferLength) { std::lock_guard lock(g_sessionMutex); RequireSession(__func__, expId); - throw std::runtime_error(std::string(__func__) + ": the host has no media library to export into"); + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceContentExportTerm(void) { diff --git a/core/libs/prx/libSceContentSearch/Export.cpp b/core/libs/prx/libSceContentSearch/Export.cpp index 0fbd052c73..10c13bfbe1 100644 --- a/core/libs/prx/libSceContentSearch/Export.cpp +++ b/core/libs/prx/libSceContentSearch/Export.cpp @@ -58,9 +58,6 @@ void ValidateOrderBy(const char* funcName, const ContentSearchContentOrderByCond } } -[[noreturn]] void ThrowUnknownMetadataId(const char* funcName, std::int32_t metadataId) { - throw std::logic_error(std::string(funcName) + ": metadata id " + std::to_string(metadataId) + " was never opened, as the host content library is empty, and the console's error for it is unknown"); -} } extern "C" { @@ -81,19 +78,22 @@ int APS5_VABI sceContentSearchCloseMetadata(std::int32_t metadataId) { int APS5_VABI sceContentSearchGetMetadataFieldInfo(std::int32_t metadataId, const char* field, std::int32_t* metadataType, std::int32_t* size) { if (!g_initialized.load()) return ErrorNotInitialized; if (field == nullptr || metadataType == nullptr || size == nullptr) return ErrorInvalidArgument; - ThrowUnknownMetadataId(__func__, metadataId); + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceContentSearchGetMetadataValue(std::int32_t metadataId, const char* field, void* value) { if (!g_initialized.load()) return ErrorNotInitialized; if (field == nullptr || value == nullptr) return ErrorInvalidArgument; - ThrowUnknownMetadataId(__func__, metadataId); + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceContentSearchOpenMetadata(const char* filePath, std::int32_t* metadataId) { if (!g_initialized.load()) return ErrorNotInitialized; if (filePath == nullptr || metadataId == nullptr) return ErrorInvalidArgument; - throw std::logic_error(std::string(__func__) + ": " + filePath + " is not in the host content library, which is empty, and the console's error for it is unknown"); + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceContentSearchSearchContent(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, const ContentSearchContentOrderByCondition* orderByConditions, std::uint32_t orderByConditionsLength, std::uint32_t offset, std::uint32_t limit, std::int64_t* numOfContent, void* infos, std::int64_t* lastUpdateId) { @@ -119,7 +119,8 @@ int APS5_VABI sceContentSearchGetContentLastUpdateId(std::int64_t* lastUpdateId) int APS5_VABI sceContentSearchOpenMetadataByContentId(std::int64_t contentId, std::int32_t* metadataId) { if (!g_initialized.load()) return ErrorNotInitialized; if (metadataId == nullptr) return ErrorInvalidArgument; - throw std::logic_error(std::string(__func__) + ": content id " + std::to_string(contentId) + " is not in the host content library, which is empty, and the console's error for it is unknown"); + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceContentSearchGetTotalContentSize(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, std::int64_t* size, std::int64_t* lastUpdateId) { diff --git a/core/libs/prx/libSceConvertKeycode/ImeKeyboardType.hpp b/core/libs/prx/libSceConvertKeycode/ImeKeyboardType.hpp index 4ace3c252c..ec7e041937 100644 --- a/core/libs/prx/libSceConvertKeycode/ImeKeyboardType.hpp +++ b/core/libs/prx/libSceConvertKeycode/ImeKeyboardType.hpp @@ -35,7 +35,7 @@ constexpr std::uint32_t LookUp(std::int32_t keyboardType, std::int32_t japaneseV constexpr std::uint32_t traditionalChinese[] = {26, 27, 28, 29, 39}; return traditionalChineseVariant >= 0 && traditionalChineseVariant < 5 ? traditionalChinese[traditionalChineseVariant] : 0; } - const std::int32_t index = keyboardType - kFirstMappedType; + const std::int64_t index = std::int64_t{keyboardType} - kFirstMappedType; return index >= 0 && index < kMappedTypeCount ? kImeTypeByKeyboardType[index] : 0; } diff --git a/core/libs/prx/libSceDiscId/CMakeLists.txt b/core/libs/prx/libSceDiscId/CMakeLists.txt new file mode 100644 index 0000000000..a1a24f3c30 --- /dev/null +++ b/core/libs/prx/libSceDiscId/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceDiscId SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceDiscId PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceDiscId) + +if(BUILD_TESTING) + add_test_executable(guest_disc_id_tests ${LIBS_INCLUDE_DIR}/tests/GuestDiscId.cpp) + target_include_directories(guest_disc_id_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_disc_id_tests PRIVATE libSceDiscId libc) + configure_windows_unwind(guest_disc_id_tests) + add_test(NAME guest_disc_id COMMAND guest_disc_id_tests) + if(WIN32) + set_tests_properties(guest_disc_id PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceDiscId/Export.cpp b/core/libs/prx/libSceDiscId/Export.cpp index 33a87a4698..b3618df723 100644 --- a/core/libs/prx/libSceDiscId/Export.cpp +++ b/core/libs/prx/libSceDiscId/Export.cpp @@ -7,11 +7,14 @@ extern "C" { int APS5_VABI sceDiscIdGet(DiscId* discId) { + (void)discId; NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceDiscIdGet2(DiscId2* discId, std::uint32_t flags) { + (void)discId; + (void)flags; NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceFiber/CMakeLists.txt b/core/libs/prx/libSceFiber/CMakeLists.txt index 883b7fc154..ef08d950c5 100644 --- a/core/libs/prx/libSceFiber/CMakeLists.txt +++ b/core/libs/prx/libSceFiber/CMakeLists.txt @@ -8,9 +8,11 @@ if(BUILD_TESTING) target_include_directories(fiber_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(fiber_tests PRIVATE libSceFiber libc) add_test(NAME fiber COMMAND fiber_tests) + add_test(NAME fiber_contextless COMMAND fiber_tests --contextless) add_asan_test_options(fiber detect_stack_use_after_return=0) + add_asan_test_options(fiber_contextless detect_stack_use_after_return=0) if(WIN32) - set_tests_properties(fiber PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(fiber fiber_contextless PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceFiber/Export.cpp b/core/libs/prx/libSceFiber/Export.cpp index 4d3e54f5f4..616919690c 100644 --- a/core/libs/prx/libSceFiber/Export.cpp +++ b/core/libs/prx/libSceFiber/Export.cpp @@ -106,14 +106,17 @@ static bool TraceFibers() { static void CompletePendingSuspend() { auto& thread = ThreadState(); if (thread.pendingSuspend) { - thread.pendingSuspend->state.store(FiberState::Suspended, std::memory_order_release); + auto* fiber = thread.pendingSuspend; + const auto state = fiber->context ? FiberState::Suspended : FiberState::Idle; + if (!fiber->context) fiber->savedStack = nullptr; + fiber->state.store(state, std::memory_order_release); thread.pendingSuspend = nullptr; } } #ifdef _WIN32 -extern "C" void Aps5FiberSwitchStack_nid_no_patch(void** save, void* load); +extern "C" void Aps5FiberSwitchStack_nid_no_patch(void** save, void** load, Fiber* start); extern "C" void Aps5FiberTrampoline_nid_no_patch(); asm(R"( @@ -143,7 +146,12 @@ Aps5FiberSwitchStack_nid_no_patch: stmxcsr 0xa0(%rsp) fnstcw 0xa4(%rsp) mov %rsp, (%rcx) - mov %rdx, %rsp + mov (%rdx), %rsp + test %r8, %r8 + jz 1f + mov %r8, %r12 + jmp Aps5FiberTrampoline_nid_no_patch +1: movaps 0x00(%rsp), %xmm6 movaps 0x10(%rsp), %xmm7 movaps 0x20(%rsp), %xmm8 @@ -214,7 +222,7 @@ static void UnpinStack(const void* context, std::uint64_t bytes) { #else -extern "C" void Aps5FiberSwitchStack_nid_no_patch(void** save, void* load); +extern "C" void Aps5FiberSwitchStack_nid_no_patch(void** save, void** load, Fiber* start); extern "C" void Aps5FiberTrampoline_nid_no_patch(); asm(".text\n" @@ -229,7 +237,12 @@ asm(".text\n" stmxcsr 0(%rsp) fnstcw 4(%rsp) mov %rsp, (%rdi) - mov %rsi, %rsp + mov (%rsi), %rsp + test %rdx, %rdx + jz 1f + mov %rdx, %r12 + jmp )" APS5_ASM_SYMBOL("Aps5FiberTrampoline_nid_no_patch") R"( +1: ldmxcsr 0(%rsp) fldcw 4(%rsp) add $8, %rsp @@ -337,6 +350,7 @@ static std::uint8_t* StackTop(const Fiber* fiber) { } static std::uint64_t* SizedWindow(const Fiber* fiber) { + if (!fiber->context) return nullptr; return reinterpret_cast(StackTop(fiber) - fiber->contextSize); } @@ -362,6 +376,10 @@ static Fiber* AsFiber(FiberObject* object) { extern "C" [[noreturn]] void Aps5FiberMain_nid_no_patch(Fiber* fiber) { CompletePendingSuspend(); + if (!fiber->context && fiber->setFpuRegisters) { + _mm_setcsr(FIBER_DEFAULT_MXCSR); + asm volatile("fldcw %0" : : "m"(FIBER_DEFAULT_FPU_CONTROL)); + } fiber->entry(fiber->argOnInitialize, ThreadState().transfer); throw std::runtime_error(std::string("sceFiber: entry function of fiber '") + fiber->name + "' returned"); } @@ -392,13 +410,7 @@ static bool AcquireForResume(Fiber* target) { } if (state != FiberState::Idle && state != FiberState::Suspended) return false; if (target->state.compare_exchange_weak(state, FiberState::Running, std::memory_order_acq_rel)) { - if (state == FiberState::Idle) { - if (!target->context) { - target->state.store(FiberState::Idle, std::memory_order_release); - throw std::runtime_error(std::string("sceFiber: fiber '") + target->name + "' has no context; running a fiber on the stack of its caller is not supported"); - } - PrepareInitialStack(target); - } + if (state == FiberState::Idle && target->context) PrepareInitialStack(target); return true; } } @@ -446,7 +458,7 @@ static int32_t AcquireWithContext(Fiber* target, void* addr_context, uint64_t si static void Resume(Fiber* target, void** save, std::uint64_t argOnRun) { const auto* frame = static_cast(target->savedStack); - if (frame->returnAddress == 0) { + if (target->context && frame->returnAddress == 0) { std::fprintf(stderr, "[fiber] resuming '%s' with a wiped context: saved=%p context=%p size=0x%llx\n", target->name, target->savedStack, static_cast(target->context), static_cast(target->contextSize)); const auto* words = static_cast(target->savedStack); @@ -456,8 +468,8 @@ static void Resume(Fiber* target, void** save, std::uint64_t argOnRun) { } ThreadState().current = target; ThreadState().transfer = argOnRun; - SetBounds(FiberBounds(target)); - Aps5FiberSwitchStack_nid_no_patch(save, target->savedStack); + SetBounds(target->context ? FiberBounds(target) : ThreadState().threadBounds); + Aps5FiberSwitchStack_nid_no_patch(save, target->context ? &target->savedStack : &ThreadState().threadStack, target->context ? nullptr : target); } static int32_t InitializeFiber(FiberObject* object, const char* name, GuestFiberEntry entry, uint64_t arg_on_initialize, void* addr_context, uint64_t size_context, uint32_t flags) { @@ -558,7 +570,7 @@ int32_t APS5_VABI sceFiberReturnToThread(uint64_t arg_on_return, uint64_t* arg_o ThreadState().current = nullptr; ThreadState().transfer = arg_on_return; SetBounds(ThreadState().threadBounds); - Aps5FiberSwitchStack_nid_no_patch(&self->savedStack, ThreadState().threadStack); + Aps5FiberSwitchStack_nid_no_patch(&self->savedStack, &ThreadState().threadStack, nullptr); CompletePendingSuspend(); if (arg_on_run) *arg_on_run = ThreadState().transfer; return SCE_OK; @@ -581,7 +593,7 @@ int32_t APS5_VABI sceFiberGetInfo(FiberObject* object, FiberInfo* fiber_info) { fiber_info->size_context = fiber->contextSize; std::memcpy(fiber_info->name, fiber->name, sizeof(fiber_info->name)); fiber_info->size_context_margin = static_cast(-1); - if (fiber->contextSizeCheck) { + if (fiber->contextSizeCheck && fiber->context) { const auto* words = SizedWindow(fiber); const auto* end = words + fiber->contextSize / sizeof(std::uint64_t); fiber_info->size_context_margin = static_cast(std::find_if(words, end, [](std::uint64_t word) { return word != FIBER_CONTEXT_FILL; }) - words) * sizeof(std::uint64_t); diff --git a/core/libs/prx/libSceFont/CMakeLists.txt b/core/libs/prx/libSceFont/CMakeLists.txt index cec16f879b..dfa37bf121 100644 --- a/core/libs/prx/libSceFont/CMakeLists.txt +++ b/core/libs/prx/libSceFont/CMakeLists.txt @@ -16,6 +16,33 @@ if(BUILD_TESTING) set_tests_properties(guest_font guest_system_font PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_font_metrics_tests ${LIBS_INCLUDE_DIR}/tests/GuestFontMetrics.cpp) + target_include_directories(guest_font_metrics_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_font_metrics_tests PRIVATE libSceFont libSceFontFt libc) + configure_windows_unwind(guest_font_metrics_tests) + add_test(NAME guest_font_metrics COMMAND guest_font_metrics_tests) + if(WIN32) + set_tests_properties(guest_font_metrics PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_font_style_information_tests ${LIBS_INCLUDE_DIR}/tests/GuestFontStyleInformation.cpp) + target_include_directories(guest_font_style_information_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_font_style_information_tests PRIVATE libSceFont libSceFontFt libc) + configure_windows_unwind(guest_font_style_information_tests) + add_test(NAME guest_font_style_information COMMAND guest_font_style_information_tests) + if(WIN32) + set_tests_properties(guest_font_style_information PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_font_outline_profile_tests ${LIBS_INCLUDE_DIR}/tests/GuestFontOutlineProfile.cpp) + target_include_directories(guest_font_outline_profile_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_font_outline_profile_tests PRIVATE libSceFont libSceFontFt libc) + configure_windows_unwind(guest_font_outline_profile_tests) + add_test(NAME guest_font_outline_profile COMMAND guest_font_outline_profile_tests) + if(WIN32) + set_tests_properties(guest_font_outline_profile PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_font_writing_line_tests ${LIBS_INCLUDE_DIR}/tests/GuestFontWritingLine.cpp) target_include_directories(guest_font_writing_line_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_font_writing_line_tests PRIVATE libSceFont libc) @@ -25,6 +52,15 @@ if(BUILD_TESTING) set_tests_properties(guest_font_writing_line PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_font_graphics_tests ${LIBS_INCLUDE_DIR}/tests/GuestFontGraphics.cpp) + target_include_directories(guest_font_graphics_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_font_graphics_tests PRIVATE libSceFont libc) + configure_windows_unwind(guest_font_graphics_tests) + add_test(NAME guest_font_graphics COMMAND guest_font_graphics_tests) + if(WIN32) + set_tests_properties(guest_font_graphics PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(font_face_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/FontFaceLifetime.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/State.cpp) target_include_directories(font_face_lifetime_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -38,4 +74,13 @@ if(BUILD_TESTING) set_tests_properties(font_face_lifetime font_face_lifetime_replace font_face_lifetime_churn PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_font_words_tests ${LIBS_INCLUDE_DIR}/tests/GuestFontWords.cpp) + target_include_directories(guest_font_words_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_font_words_tests PRIVATE libSceFont libc) + configure_windows_unwind(guest_font_words_tests) + add_test(NAME guest_font_words COMMAND guest_font_words_tests) + if(WIN32) + set_tests_properties(guest_font_words PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceFont/FontLibrary.cmake b/core/libs/prx/libSceFont/FontLibrary.cmake index 47bf723165..0ae75346fe 100644 --- a/core/libs/prx/libSceFont/FontLibrary.cmake +++ b/core/libs/prx/libSceFont/FontLibrary.cmake @@ -1,6 +1,7 @@ function(add_sce_font_library target) set(fontDir ${CMAKE_CURRENT_FUNCTION_LIST_DIR}) add_library(${target} SHARED EXCLUDE_FROM_ALL + ${fontDir}/src/Graphics.cpp ${fontDir}/src/Layout.cpp ${fontDir}/src/Library.cpp ${fontDir}/src/Open.cpp @@ -10,6 +11,7 @@ function(add_sce_font_library target) ${fontDir}/src/SystemFont.cpp ${fontDir}/src/Text.cpp ${fontDir}/src/Unimplemented.cpp + ${fontDir}/src/Words.cpp ${fontDir}/src/WritingLine.cpp ) target_include_directories(${target} PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceFont/include/Api.hpp b/core/libs/prx/libSceFont/include/Api.hpp index bdb0f3f449..2e7d3d5ca6 100644 --- a/core/libs/prx/libSceFont/include/Api.hpp +++ b/core/libs/prx/libSceFont/include/Api.hpp @@ -128,6 +128,12 @@ struct FontGraphicsDeviceUsage { FontGraphicsMemoryUsage second; }; +namespace Font { + +using GraphicsErrorCallback = void (APS5_VABI*)(int error); + +} + extern "C" { int APS5_VABI sceFontMemoryInit(FontMemory* memory, void* regionAddress, std::uint32_t regionSize, const FontMemoryInterface* iface, void* mspaceObject, FontMemoryDestroyFunction destroyCallback, void* destroyObject); @@ -147,6 +153,7 @@ int APS5_VABI sceFontClearDeviceCache(FontLibrary library); int APS5_VABI sceFontDettachDeviceCacheBuffer(FontLibrary library, void** buffer, std::uint32_t* size); int APS5_VABI sceFontSupportExternalFonts(FontLibrary library, std::uint32_t fontMax, std::uint32_t formats); int APS5_VABI sceFontSupportSystemFonts(FontLibrary library); +void APS5_VABI sceFontSetGraphicsErrorCallback(Font::GraphicsErrorCallback callback); int APS5_VABI sceFontSetFontsOpenMode(FontLibrary library, std::uint32_t openMode); int APS5_VABI sceFontOpenFontMemory(FontLibrary library, const void* fontAddress, std::uint32_t fontSize, const FontOpenDetail* openDetail, FontHandle* pFontHandle); int APS5_VABI sceFontOpenFontFile(FontLibrary library, const char* path, std::uint32_t openMode, const FontOpenDetail* openDetail, FontHandle* pFontHandle); diff --git a/core/libs/prx/libSceFont/include/FontDriver.hpp b/core/libs/prx/libSceFont/include/FontDriver.hpp index 35b8d3d533..70b562b215 100644 --- a/core/libs/prx/libSceFont/include/FontDriver.hpp +++ b/core/libs/prx/libSceFont/include/FontDriver.hpp @@ -208,6 +208,9 @@ using DriverGlyphsCountFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32 using DriverGlyphIndexFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t codepoint, std::uint32_t* outGlyphIndex); using DriverSetCharWithDpiFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t dpiX, std::uint32_t dpiY, float scaleX, float scaleY, float* outScaleX, float* outScaleY); using DriverSetCharDefaultDpiFunction = int (APS5_VABI*)(FontObj* fontObj, float scaleX, float scaleY, float* outScaleX, float* outScaleY); +using DriverFontMetricsFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t form, FontMetrics* outMetrics); +using DriverOutlineProfileFunction = int (APS5_VABI*)(FontObj* fontObj, FontOutlineProfile* outProfile); +using DriverStyleInformationFunction = int (APS5_VABI*)(FontObj* fontObj, FontStyleInformation* outInformation); using DriverComputeLayoutFunction = int (APS5_VABI*)(FontObj* fontObj, const StyleStateBlock* style, std::uint8_t* outWords); using DriverLoadGlyphCachedFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t glyphIndex, std::int32_t mode, std::uint64_t* outWords); using DriverGetGlyphMetricsFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t* optParam2, std::uint8_t mode, std::uint8_t* outParams, FontGlyphMetrics* outMetrics); @@ -225,16 +228,17 @@ struct SysDriver { std::uint8_t reserved_0x30[0x08]; DriverOpenFunction open; DriverCloseFunction close; - std::uint8_t reserved_0x48[0x08]; + DriverStyleInformationFunction style_information; DriverScaleFunction scale; std::uint8_t reserved_0x58[0x08]; DriverMetricFunction metric; DriverGlyphsCountFunction glyphs_count; - std::uint8_t reserved_0x70[0x08]; + DriverOutlineProfileFunction outline_profile; DriverGlyphIndexFunction glyph_index; DriverSetCharWithDpiFunction set_char_with_dpi; DriverSetCharDefaultDpiFunction set_char_default_dpi; - std::uint8_t reserved_0x90[0x10]; + std::uint8_t reserved_0x90[0x08]; + DriverFontMetricsFunction font_metrics; DriverComputeLayoutFunction compute_layout; DriverLoadGlyphCachedFunction load_glyph_cached; std::uint8_t reserved_0xb0[0x08]; @@ -341,8 +345,11 @@ static_assert(offsetof(FontLibNative, list_head) == 0xE0); static_assert(sizeof(SysDriver) == 0x120); static_assert(offsetof(SysDriver, pixel_resolution) == 0x10); static_assert(offsetof(SysDriver, open) == 0x38); +static_assert(offsetof(SysDriver, style_information) == 0x48); static_assert(offsetof(SysDriver, scale) == 0x50); +static_assert(offsetof(SysDriver, outline_profile) == 0x70); static_assert(offsetof(SysDriver, glyph_index) == 0x78); +static_assert(offsetof(SysDriver, font_metrics) == 0x98); static_assert(offsetof(SysDriver, compute_layout) == 0xA0); static_assert(offsetof(SysDriver, get_glyph_metrics) == 0xB8); static_assert(offsetof(SysDriver, apply_glyph_adjust) == 0xE0); diff --git a/core/libs/prx/libSceFont/include/FontGraphics.hpp b/core/libs/prx/libSceFont/include/FontGraphics.hpp new file mode 100644 index 0000000000..a5dbe25b02 --- /dev/null +++ b/core/libs/prx/libSceFont/include/FontGraphics.hpp @@ -0,0 +1,25 @@ +#ifndef CORE_LIBS_PRX_LIBSCEFONT_INCLUDE_FONTGRAPHICS_HPP +#define CORE_LIBS_PRX_LIBSCEFONT_INCLUDE_FONTGRAPHICS_HPP + +#include + +#include "prx/libSceFont/include/Api.hpp" + +namespace Font { + +void SetGraphicsErrorCallback(GraphicsErrorCallback callback); +int GraphicsRegionInit(FontGraphicsRegion* region, float x, float y, float width, float height); +int GraphicsRegionInitCircular(FontGraphicsRegion* region, float centerX, float centerY, float outerRadius, float innerRadius); +int GraphicsRegionInitRoundish(FontGraphicsRegion* region, float x, float y, float width, float height, float outerRadius, float innerRadius, std::uint32_t flags); +int GraphicsFillMethodInit(FontGraphicsFillMethod* method, std::int32_t mode, std::int32_t type); +int GraphicsFillPlotInit(FontGraphicsFillPlot* plot, const FontGraphicsRegion* region); +int GraphicsFillPlotSetLayout(FontGraphicsFillPlot* plot, std::int32_t first, std::int32_t second); +int GraphicsFillPlotSetMapping(FontGraphicsFillPlot* plot, float first, float second, float third, float fourth); +int GraphicsFillRatesInit(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect); +int GraphicsFillRatesSetFillEffect(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect); +int GraphicsFillRatesSetLayout(FontGraphicsFillRates* rates, std::int32_t first, std::int32_t second); +int GraphicsFillRatesSetMapping(FontGraphicsFillRates* rates, float first, float second, float third, float fourth); + +} + +#endif diff --git a/core/libs/prx/libSceFont/include/FontInternal.hpp b/core/libs/prx/libSceFont/include/FontInternal.hpp index 54c173835e..da4a70b28a 100644 --- a/core/libs/prx/libSceFont/include/FontInternal.hpp +++ b/core/libs/prx/libSceFont/include/FontInternal.hpp @@ -133,6 +133,9 @@ int ComputeHorizontalLayout(FontHandle handle, const StyleStateBlock* style, std int ComputeVerticalLayout(FontHandle handle, const StyleStateBlock* style, std::uint8_t* outWords); int GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMetrics* metrics, bool useCachedStyle, float* usedScaleW = nullptr, float* usedScaleH = nullptr); int RenderCharGlyphImageCore(FontHandle handle, std::uint32_t code, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result); +int ComputeFontMetrics(FontHandle handle, std::uint32_t form, FontMetrics* metrics); +int ComputeFontStyleInformation(FontHandle handle, FontStyleInformation* information); +int ComputeFontGlyphsOutlineProfile(FontHandle handle, FontOutlineProfile* profile); } diff --git a/core/libs/prx/libSceFont/include/FontTypes.hpp b/core/libs/prx/libSceFont/include/FontTypes.hpp index eb1a548bb4..6507485349 100644 --- a/core/libs/prx/libSceFont/include/FontTypes.hpp +++ b/core/libs/prx/libSceFont/include/FontTypes.hpp @@ -423,12 +423,55 @@ struct FontVerticalLayout { float decorationSpan; }; +struct FontMetrics { + std::uint32_t form; + float ascender; + float descender; + float lineGap; + float maxAdvance; + float verticalDescender; + float verticalAscender; + float xMin; + float yMin; + float xMax; + float yMax; + float emWidth; + float emHeight; + float effectWeightX; + float effectWeightY; + float slantRatio; +}; + +struct FontOutlineProfile { + std::uint32_t maxContours; + std::uint32_t maxPoints; + std::uint32_t maxCompositeContours; + std::uint32_t maxCompositePoints; +}; + +struct FontStyleInformation { + const char* familyName; + const char* styleName; + std::uint16_t styleFlags; + std::uint16_t weight; + std::uint16_t width; + std::uint16_t layoutFlags; +}; + static_assert(sizeof(FontHandleOpaque) == 0x100); static_assert(sizeof(FontMemory) == 0x40); static_assert(sizeof(FontMemoryInterface) == 0x30); static_assert(sizeof(FontOpenDetail) == 0x20); static_assert(sizeof(FontGlyphMetrics) == 0x20); static_assert(sizeof(FontKerning) == 0x10); +static_assert(sizeof(FontMetrics) == 0x40); +static_assert(offsetof(FontMetrics, maxAdvance) == 0x10); +static_assert(offsetof(FontMetrics, xMin) == 0x1C); +static_assert(offsetof(FontMetrics, emWidth) == 0x2C); +static_assert(offsetof(FontMetrics, effectWeightX) == 0x34); +static_assert(sizeof(FontStyleInformation) == 0x18); +static_assert(sizeof(FontOutlineProfile) == 0x10); +static_assert(offsetof(FontStyleInformation, styleFlags) == 0x10); static_assert(sizeof(FontGlyphImageMetrics) == 0x18); static_assert(sizeof(FontGenerateGlyphDetail) == 0x20); static_assert(sizeof(FontResultStage) == 0x18); diff --git a/core/libs/prx/libSceFont/include/FontWords.hpp b/core/libs/prx/libSceFont/include/FontWords.hpp new file mode 100644 index 0000000000..d40842f3f8 --- /dev/null +++ b/core/libs/prx/libSceFont/include/FontWords.hpp @@ -0,0 +1,16 @@ +#ifndef CORE_LIBS_PRX_LIBSCEFONT_INCLUDE_FONTWORDS_HPP +#define CORE_LIBS_PRX_LIBSCEFONT_INCLUDE_FONTWORDS_HPP + +#include "prx/libSceFont/include/FontTypes.hpp" + +struct FontWords; + +namespace Font { + +int CreateWords(const FontMemory* memory, FontTextSource* source, FontWords** words); +int DestroyWords(FontWords** words); +int WordsFindWordCharacters(FontWords* words, FontTextCharacter* firstCharacter, FontTextCharacter* endCharacter, FontTextCharacter** lastCharacter, FontTextCharacter** nextCharacter); + +} + +#endif diff --git a/core/libs/prx/libSceFont/src/Graphics.cpp b/core/libs/prx/libSceFont/src/Graphics.cpp new file mode 100644 index 0000000000..8a005d478a --- /dev/null +++ b/core/libs/prx/libSceFont/src/Graphics.cpp @@ -0,0 +1,195 @@ +#include +#include + +#include "prx/libSceFont/include/FontGraphics.hpp" +#include "prx/libSceFont/include/FontTypes.hpp" + +namespace { + +constexpr int FONT_GRAPHICS_ERROR_INVALID_FILL = static_cast(0x80460086); +constexpr int FONT_GRAPHICS_ERROR_INVALID_REGION = static_cast(0x80460087); +constexpr int FONT_GRAPHICS_ERROR_INVALID_FILL_PLOT = static_cast(0x80460088); +constexpr int FONT_GRAPHICS_ERROR_INVALID_FILL_RATES = static_cast(0x80460089); + +constexpr std::uint32_t RegionTag = 0xf60; +constexpr std::uint32_t RectangleRegionFlags = 0xf; +constexpr std::uint32_t CircularRegionFlags = 0xff; +constexpr std::uint32_t FillMethodTag = 0xf01b; +constexpr std::uint32_t FillMethodFlags = 0xf; +constexpr std::uint16_t FillPlotTag = 0xf01c; +constexpr std::uint16_t FillRatesTag = 0xf01a; +constexpr std::uint16_t DefaultLayout = 0xf77; +constexpr std::uint32_t FillEffects = 0x1033f; + +std::atomic g_errorCallback{nullptr}; + +int Fail(int error) { + if (const auto callback = g_errorCallback.load()) callback(error); + return error; +} + +bool IsObject(const void* pointer) { + return reinterpret_cast(pointer) + 1 >= 2; +} + +bool IsValidFill(const FontGraphicsFill* fill) { + const auto value = reinterpret_cast(fill); + if (value >= -1 && value < 3) return true; + const std::uint8_t mode = *reinterpret_cast(fill); + return (mode & 0xf) == 0 && mode >= 0x10 && mode <= 0x50; +} + +bool IsValidEffect(std::uint32_t effect) { + return effect < 17 && ((FillEffects >> effect) & 1) != 0; +} + +bool IsValidLayoutNibble(std::uint32_t nibble) { + return nibble <= 4 && nibble != 3; +} + +bool IsValidLayout(std::int32_t first, std::int32_t second) { + const bool firstValid = first == 0 || first == 0x170 || first == 0x207 || first == 0x400 || first == 0x800 || first == 0xf77; + const auto bits = static_cast(second); + return firstValid && IsValidLayoutNibble((bits >> 4) & 0xf) && IsValidLayoutNibble(bits & 0xf); +} + +bool IsUsableRegion(const FontGraphicsRegion& region) { + if (region.tag != RegionTag) return false; + if (!(region.outerRadius >= 0.0f) || !(region.innerRadius >= 0.0f)) return false; + const float half = (region.width < region.height ? region.width : region.height) * 0.5f; + if (region.outerRadius == 0.0f) return half >= region.innerRadius; + return half >= region.outerRadius && region.outerRadius >= region.innerRadius; +} + +std::uint8_t MappedFlags(std::uint8_t flags) { + return static_cast((flags & 0xfc) | 1); +} + +} + +namespace Font { + +void SetGraphicsErrorCallback(GraphicsErrorCallback callback) { + g_errorCallback.store(callback); +} + +int GraphicsRegionInit(FontGraphicsRegion* region, float x, float y, float width, float height) { + if (!region || 0.0f > width || 0.0f > height) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + *region = {x, y, width, height, RectangleRegionFlags, RegionTag, 0.0f, 0.0f}; + return SCE_FONT_OK; +} + +int GraphicsRegionInitCircular(FontGraphicsRegion* region, float centerX, float centerY, float outerRadius, float innerRadius) { + if (innerRadius > outerRadius || !region || 0.0f >= outerRadius || 0.0f >= innerRadius) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + const float diameter = outerRadius + outerRadius; + *region = {centerX - outerRadius, centerY - outerRadius, diameter, diameter, CircularRegionFlags, RegionTag, outerRadius, innerRadius}; + return SCE_FONT_OK; +} + +int GraphicsRegionInitRoundish(FontGraphicsRegion* region, float x, float y, float width, float height, float outerRadius, float innerRadius, std::uint32_t flags) { + if (!region || 0.0f > innerRadius || 0.0f > outerRadius || 0.0f >= height || 0.0f >= width || flags > 0xff) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + if (outerRadius > height * 0.5f || outerRadius > width * 0.5f) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + if (outerRadius < innerRadius && outerRadius != 0.0f) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + *region = {x, y, width, height, flags, RegionTag, outerRadius, innerRadius}; + return SCE_FONT_OK; +} + +int GraphicsFillMethodInit(FontGraphicsFillMethod* method, std::int32_t mode, std::int32_t type) { + const auto modeBits = static_cast(mode); + const bool modeValid = (modeBits & 0xf) == 0 && (modeBits >> 4) < 6; + const bool typeValid = static_cast(type) - 1 < 7 || type == 12; + if (!method || !modeValid || !typeValid) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + *method = {}; + method->mode = mode; + method->type = type; + method->flags = FillMethodFlags; + method->tag = FillMethodTag; + return SCE_FONT_OK; +} + +int GraphicsFillPlotInit(FontGraphicsFillPlot* plot, const FontGraphicsRegion* region) { + if (!IsObject(plot)) return Fail(FONT_GRAPHICS_ERROR_INVALID_FILL_PLOT); + if (IsObject(region) && !IsUsableRegion(*region)) { + plot->tag = 0; + return Fail(FONT_GRAPHICS_ERROR_INVALID_REGION); + } + *plot = {}; + if (!region) { + plot->flags = 2; + } else if (!IsObject(region)) { + plot->flags = 6; + plot->layout = DefaultLayout; + } else { + plot->mapping[0] = region->x; + plot->mapping[1] = region->y; + plot->mapping[2] = region->x + region->width; + plot->mapping[3] = region->y + region->height; + plot->flags = 1; + plot->layout = DefaultLayout; + } + plot->region = region; + plot->tag = FillPlotTag; + return SCE_FONT_OK; +} + +int GraphicsFillPlotSetLayout(FontGraphicsFillPlot* plot, std::int32_t first, std::int32_t second) { + if (!IsObject(plot) || plot->tag != FillPlotTag) return Fail(FONT_GRAPHICS_ERROR_INVALID_FILL_PLOT); + if (!IsValidLayout(first, second)) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + const auto combined = static_cast(first) | static_cast(second); + plot->layout = static_cast(combined); + plot->flags = static_cast((plot->flags & 0xf6) | (combined != 0 ? 1 : 0) | 8); + return SCE_FONT_OK; +} + +int GraphicsFillPlotSetMapping(FontGraphicsFillPlot* plot, float first, float second, float third, float fourth) { + if (!IsObject(plot) || plot->tag != FillPlotTag) return Fail(FONT_GRAPHICS_ERROR_INVALID_FILL_PLOT); + plot->mapping[0] = first; + plot->mapping[1] = second; + plot->mapping[2] = third; + plot->mapping[3] = fourth; + plot->flags = MappedFlags(plot->flags); + return SCE_FONT_OK; +} + +int GraphicsFillRatesInit(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect) { + if (!IsObject(rates)) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + *rates = {}; + if (!IsValidFill(fill)) return Fail(FONT_GRAPHICS_ERROR_INVALID_FILL); + if (!IsValidEffect(effect)) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + rates->fill = fill; + rates->tag = FillRatesTag; + rates->effect = static_cast(effect); + rates->layout = DefaultLayout; + rates->flags = 2; + return SCE_FONT_OK; +} + +int GraphicsFillRatesSetFillEffect(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect) { + if (!IsObject(rates) || rates->tag != FillRatesTag) return Fail(FONT_GRAPHICS_ERROR_INVALID_FILL_RATES); + if (!IsValidFill(fill)) return Fail(FONT_GRAPHICS_ERROR_INVALID_FILL); + if (!IsValidEffect(effect)) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + rates->effect = static_cast(effect); + rates->fill = fill; + return SCE_FONT_OK; +} + +int GraphicsFillRatesSetLayout(FontGraphicsFillRates* rates, std::int32_t first, std::int32_t second) { + if (!IsObject(rates) || rates->tag != FillRatesTag) return Fail(FONT_GRAPHICS_ERROR_INVALID_FILL_RATES); + if (!IsValidLayout(first, second)) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + const auto combined = static_cast(first) | static_cast(second); + rates->flags = static_cast((rates->flags & 0xfe) | (combined != 0 ? 1 : 0)); + rates->layout = 0; + return SCE_FONT_OK; +} + +int GraphicsFillRatesSetMapping(FontGraphicsFillRates* rates, float first, float second, float third, float fourth) { + if (!IsObject(rates) || rates->tag != FillRatesTag) return Fail(FONT_GRAPHICS_ERROR_INVALID_FILL_RATES); + rates->mapping[0] = first; + rates->mapping[1] = second; + rates->mapping[2] = third; + rates->mapping[3] = fourth; + rates->flags = MappedFlags(rates->flags); + return SCE_FONT_OK; +} + +} diff --git a/core/libs/prx/libSceFont/src/Layout.cpp b/core/libs/prx/libSceFont/src/Layout.cpp index 8bce6e00c2..d297840675 100644 --- a/core/libs/prx/libSceFont/src/Layout.cpp +++ b/core/libs/prx/libSceFont/src/Layout.cpp @@ -530,3 +530,109 @@ bool Font::BuildTrueOutline(GeneratedGlyph& glyph) { glyph.outlineInitialized = true; return true; } + +int Font::ComputeFontMetrics(FontHandle handle, std::uint32_t form, FontMetrics* metrics) { + auto* font = GetNativeFont(handle); + auto* library = font ? static_cast(font->library) : nullptr; + if (!library || !FontContext(library, font)) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + const SysDriver* driver = library->sys_driver; + if (!driver || !driver->set_char_with_dpi || !driver->set_char_default_dpi || !driver->font_metrics) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + const std::uint32_t fontId = font->open_info.ctx_entry_index; + if (static_cast(fontId) < 0) { + metrics->form = form; + metrics->ascender = 0.0f; + metrics->descender = 0.0f; + metrics->lineGap = 0.0f; + metrics->maxAdvance = 0.0f; + metrics->verticalDescender = 0.0f; + metrics->verticalAscender = 0.0f; + return SCE_FONT_OK; + } + const std::uint32_t modeLow = font->flags & 0x0Fu; + FontObj* head = nullptr; + std::uint32_t lockWord = 0; + auto* entry = AcquireFontCtxEntry(FontContext(library, font), fontId, modeLow, &head, &lockWord); + if (!entry) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + FontObj* match = FindSubFont(head, font->open_info.sub_font_index); + int rc = SCE_FONT_ERROR_FATAL; + if ((lockWord & OPEN_BIT) != 0 && (lockWord & COUNT_MASK) != 0 && match) { + float scaleX = font->style.scale_w; + float scaleY = font->style.scale_h; + match->font_handle = handle; + match->shift_units_x = 0; + match->shift_units_y = 0; + rc = SetDriverCharSize(driver, match, &font->style, scaleX, scaleY); + if (rc == SCE_FONT_OK) rc = driver->font_metrics(match, form, metrics); + } + ReleaseFontCtxEntryLock(entry, modeLow, lockWord); + return rc == SCE_FONT_OK ? rc : SCE_FONT_ERROR_INVALID_FONT_HANDLE; +} + +int Font::ComputeFontStyleInformation(FontHandle handle, FontStyleInformation* information) { + auto* font = GetNativeFont(handle); + auto* library = static_cast(font->library); + if (!library) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + if (!information) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (const auto* record = static_cast(font->open_info.fontset_record)) { + std::memcpy(&information->familyName, record + 8, sizeof(information->familyName)); + std::memcpy(&information->styleName, record + 16, sizeof(information->styleName)); + information->styleFlags = record[4]; + information->weight = static_cast(record[5] * 100); + information->width = record[6]; + information->layoutFlags = record[7]; + return SCE_FONT_OK; + } + const SysDriver* driver = library->sys_driver; + auto* context = FontContext(library, font); + if (!driver || !driver->style_information || !context) return SCE_FONT_ERROR_FATAL; + const std::uint32_t modeLow = font->flags & 0x0Fu; + FontObj* head = nullptr; + std::uint32_t lockWord = 0; + auto* entry = AcquireFontCtxEntry(context, font->open_info.ctx_entry_index, modeLow, &head, &lockWord); + if (!entry) return SCE_FONT_ERROR_FATAL; + FontObj* match = FindSubFont(head, font->open_info.sub_font_index); + int rc = SCE_FONT_ERROR_FATAL; + if ((lockWord & OPEN_BIT) != 0 && (lockWord & COUNT_MASK) != 0 && match) { + match->font_handle = handle; + rc = driver->style_information(match, information); + information->weight = static_cast(information->weight * 100); + } + ReleaseFontCtxEntryLock(entry, modeLow, lockWord); + return rc; +} + +int Font::ComputeFontGlyphsOutlineProfile(FontHandle handle, FontOutlineProfile* profile) { + auto* font = GetNativeFont(handle); + if (!font || font->magic != HANDLE_MAGIC) { + if (profile) *profile = {}; + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + } + if (!profile) return SCE_FONT_ERROR_INVALID_PARAMETER; + *profile = {}; + auto* library = static_cast(font->library); + auto* context = library ? FontContext(library, font) : nullptr; + if (!context) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + const std::uint32_t fontId = font->open_info.ctx_entry_index; + if (static_cast(fontId) < 0) return SCE_FONT_OK; + const SysDriver* driver = library->sys_driver; + if (!driver || !driver->outline_profile) return SCE_FONT_ERROR_FATAL; + const std::uint32_t modeLow = font->flags & 0x0Fu; + FontObj* head = nullptr; + std::uint32_t lockWord = 0; + auto* entry = AcquireFontCtxEntry(context, fontId, modeLow, &head, &lockWord); + if (!entry) return SCE_FONT_ERROR_FATAL; + FontObj* match = FindSubFont(head, font->open_info.sub_font_index); + int rc = SCE_FONT_ERROR_FATAL; + FontOutlineProfile faceProfile{}; + if ((lockWord & OPEN_BIT) != 0 && (lockWord & COUNT_MASK) != 0 && match) { + match->font_handle = handle; + rc = driver->outline_profile(match, &faceProfile); + } + ReleaseFontCtxEntryLock(entry, modeLow, lockWord); + if (rc != SCE_FONT_OK) return rc; + profile->maxContours = std::max(profile->maxContours, faceProfile.maxContours); + profile->maxPoints = std::max(profile->maxPoints, faceProfile.maxPoints); + profile->maxCompositeContours = std::max(profile->maxCompositeContours, faceProfile.maxCompositeContours); + profile->maxCompositePoints = std::max(profile->maxCompositePoints, faceProfile.maxCompositePoints); + return SCE_FONT_OK; +} diff --git a/core/libs/prx/libSceFont/src/Render.cpp b/core/libs/prx/libSceFont/src/Render.cpp index 876b21a590..0647f2192e 100644 --- a/core/libs/prx/libSceFont/src/Render.cpp +++ b/core/libs/prx/libSceFont/src/Render.cpp @@ -159,6 +159,63 @@ int APS5_VABI sceFontGetHorizontalLayout(FontHandle fontHandle, FontHorizontalLa return SCE_FONT_OK; } +int APS5_VABI sceFontGetFontMetrics(FontHandle fontHandle, std::uint32_t form, FontMetrics* metrics) { + auto* font = GetNativeFont(fontHandle); + std::uint32_t fontLock = 0; + if (font && font->magic == HANDLE_MAGIC && AcquireFontLock(font, fontLock)) { + if (!metrics) { + ReleaseFontLock(font, fontLock); + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + } + const int rc = ComputeFontMetrics(fontHandle, form, metrics); + ReleaseFontLock(font, fontLock); + if (rc == SCE_FONT_OK) return rc; + } + if (metrics) { + *metrics = {}; + metrics->effectWeightX = 1.0f; + metrics->effectWeightY = 1.0f; + } + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; +} + +int APS5_VABI sceFontGetFontStyleInformation(FontHandle fontHandle, FontStyleInformation* information) { + auto* font = GetNativeFont(fontHandle); + std::uint32_t fontLock = 0; + int rc = SCE_FONT_ERROR_INVALID_FONT_HANDLE; + if (font && font->magic == HANDLE_MAGIC && AcquireFontLock(font, fontLock)) { + rc = ComputeFontStyleInformation(fontHandle, information); + ReleaseFontLock(font, fontLock); + if (rc == SCE_FONT_OK) return rc; + } + if (information) { + information->familyName = ""; + information->styleName = ""; + information->styleFlags = 0; + information->weight = 0; + information->width = 0; + information->layoutFlags = 0; + } + return rc; +} + +int APS5_VABI sceFontGetFontGlyphsOutlineProfile(FontHandle fontHandle, FontOutlineProfile* profile) { + auto* font = GetNativeFont(fontHandle); + std::uint32_t fontLock = 0; + int rc = SCE_FONT_ERROR_INVALID_FONT_HANDLE; + if (font && font->magic == HANDLE_MAGIC) { + if (!profile) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (font->open_info.fontset_record) NotImplemented_nid_no_patch(__func__); + if (AcquireFontLock(font, fontLock)) { + rc = ComputeFontGlyphsOutlineProfile(fontHandle, profile); + ReleaseFontLock(font, fontLock); + if (rc == SCE_FONT_OK) return rc; + } + } + if (profile) *profile = {}; + return rc; +} + int APS5_VABI sceFontGetVerticalLayout(FontHandle fontHandle, FontVerticalLayout* layout) { int rc = SCE_FONT_ERROR_INVALID_FONT_HANDLE; auto* font = GetNativeFont(fontHandle); diff --git a/core/libs/prx/libSceFont/src/SystemFont.cpp b/core/libs/prx/libSceFont/src/SystemFont.cpp index c6e3d87339..12d39768c2 100644 --- a/core/libs/prx/libSceFont/src/SystemFont.cpp +++ b/core/libs/prx/libSceFont/src/SystemFont.cpp @@ -60,7 +60,7 @@ constexpr std::array FAMILY_FILES = {{ {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 0}, {{"DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf"}, {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 2}, - {{"SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Bold.otf"}, + {{"SCEPS4Yoongd-Light.otf", "SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Bold.otf"}, {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 1}, {{"SSTArabic-Light.otf", "SSTArabic-Roman.otf", "SSTArabic-Medium.otf", "SSTArabic-Bold.otf"}, {"NotoSansArabic-Light.ttf", "NotoSansArabic-Regular.ttf", "NotoSansArabic-Medium.ttf", "NotoSansArabic-Bold.ttf"}, 0}, diff --git a/core/libs/prx/libSceFont/src/Text.cpp b/core/libs/prx/libSceFont/src/Text.cpp index 115bf3887a..0b6823f3b3 100644 --- a/core/libs/prx/libSceFont/src/Text.cpp +++ b/core/libs/prx/libSceFont/src/Text.cpp @@ -21,6 +21,7 @@ constexpr std::uint16_t FONT_STRING_MAGIC = 0x0F05; constexpr std::uint16_t FONT_WRITING_MAGIC = 0x0F06; constexpr std::uint16_t CREATE_STRING_DETAIL_ID = 0x0FD4; constexpr std::uint32_t WRITING_FORM_HORIZONTAL = 0x10; +constexpr std::uint32_t WRITING_FORM_HORIZONTAL_RTL = 0x11; constexpr std::uint32_t WRITING_FORM_HORIZONTAL_LTR = 0x12; constexpr std::int32_t PARSER_RESULT_ERROR = -1; constexpr std::int32_t PARSER_RESULT_TERMINATE = 0; @@ -63,6 +64,7 @@ struct FontWritingData { float penY = 0.0f; bool hasAggregateMetrics = false; bool hasCurrentStep = false; + bool hasCurrentCharacter = false; bool maskFormatCharacters = true; bool ownsStorage = false; bool hasRawGlyphMetrics = false; @@ -144,6 +146,37 @@ FontTextCharacter* TextCharacters(FontStringData* data) { return data && data->storage && !data->storage->characters.empty() ? data->storage->characters.data() : nullptr; } +bool IsRightToLeftLetter(std::uint32_t code) { + if ((code >= 0x0660u && code <= 0x0669u) || (code >= 0x06F0u && code <= 0x06F9u)) return false; + return (code >= 0x0590u && code <= 0x08FFu) || (code >= 0xFB1Du && code <= 0xFDFFu) || (code >= 0xFE70u && code <= 0xFEFFu) || + (code >= 0x10800u && code <= 0x10FFFu) || (code >= 0x1E800u && code <= 0x1EFFFu); +} + +bool IsDirectionNeutral(std::uint32_t code) { + return code < 0x41u || (code >= 0x5Bu && code <= 0x60u) || (code >= 0x7Bu && code <= 0xBFu) || code == 0xD7u || code == 0xF7u || + (code >= 0x2000u && code <= 0x2BFFu) || (code >= 0x3000u && code <= 0x3004u) || (code >= 0xFF00u && code <= 0xFF20u); +} + +std::uint32_t DefaultStringForm(const std::vector& characters) { + std::uint32_t isolateDepth = 0; + for (const FontTextCharacter& character : characters) { + const std::uint32_t code = character.characterCode; + if (code >= 0x2066u && code <= 0x2068u) { + ++isolateDepth; + continue; + } + if (code == 0x2069u) { + if (isolateDepth != 0) --isolateDepth; + continue; + } + if (isolateDepth != 0) continue; + if (code == 0x200Eu) return WRITING_FORM_HORIZONTAL_LTR; + if (code == 0x200Fu || IsRightToLeftLetter(code)) return WRITING_FORM_HORIZONTAL_RTL; + if (!IsDirectionNeutral(code)) return WRITING_FORM_HORIZONTAL_LTR; + } + return WRITING_FORM_HORIZONTAL_LTR; +} + std::uint32_t LevelOf(const FontTextCharacter* character) { return static_cast((character->flags >> 24) & 0xFFu); } @@ -250,7 +283,8 @@ int APS5_VABI sceFontTextSourceInit(FontTextSource* textSource, const void* text } int APS5_VABI sceFontTextSourceRewind(FontTextSource* textSource) { - if (!textSource || TextSourceMagic(textSource) != TEXT_SOURCE_MAGIC) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (!textSource) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (TextSourceMagic(textSource) != TEXT_SOURCE_MAGIC) return SCE_FONT_ERROR_INVALID_TEXT_SOURCE; textSource->start = textSource->systemUse[0]; textSource->end = textSource->systemUse[1]; textSource->current = textSource->systemUse[0]; @@ -261,14 +295,16 @@ int APS5_VABI sceFontTextSourceRewind(FontTextSource* textSource) { } int APS5_VABI sceFontTextSourceSetDefaultFont(FontTextSource* textSource, FontHandle defaultFont) { - if (!textSource || TextSourceMagic(textSource) != TEXT_SOURCE_MAGIC) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (!textSource) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (TextSourceMagic(textSource) != TEXT_SOURCE_MAGIC) return SCE_FONT_ERROR_INVALID_TEXT_SOURCE; textSource->defaultFont = defaultFont; textSource->systemUse[4] = defaultFont; return SCE_FONT_OK; } int APS5_VABI sceFontTextSourceSetWritingForm(FontTextSource* textSource, std::int32_t writingForm) { - if (!textSource || TextSourceMagic(textSource) != TEXT_SOURCE_MAGIC) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (!textSource) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (TextSourceMagic(textSource) != TEXT_SOURCE_MAGIC) return SCE_FONT_ERROR_INVALID_TEXT_SOURCE; if (writingForm < static_cast(WRITING_FORM_HORIZONTAL) || writingForm > static_cast(WRITING_FORM_HORIZONTAL_LTR)) return SCE_FONT_ERROR_INVALID_PARAMETER; textSource->systemUse0 = (textSource->systemUse0 & 0xFFFFFFFFull) | (static_cast(writingForm) << 32); return SCE_FONT_OK; @@ -327,6 +363,7 @@ int APS5_VABI sceFontCreateString(const FontMemory* fontMemory, FontTextSource* fontMemory->iface->dealloc(fontMemory->mspace_handle, raw); return state; } + if (data->writingForm == WRITING_FORM_HORIZONTAL) data->writingForm = DefaultStringForm(data->storage->characters); LinkCharacters(data->storage->characters); data->textCount = static_cast(data->storage->characters.size()); data->renderCount = data->textCount; @@ -651,6 +688,7 @@ const FontWritingStep* APS5_VABI sceFontWritingRefersRenderStep(FontWriting* fon if (!PopulateWritingStep(*writing, writing->nextIndex)) return nullptr; writing->currentIndex = writing->nextIndex; writing->hasCurrentStep = true; + writing->hasCurrentCharacter = true; AccumulateWritingMetrics(*writing); ++writing->nextIndex; return &writing->step; @@ -659,7 +697,8 @@ const FontWritingStep* APS5_VABI sceFontWritingRefersRenderStep(FontWriting* fon FontTextCharacter* APS5_VABI sceFontWritingRefersRenderStepCharacter(FontWriting* fontWriting, const FontWritingLetterStep** pLetterStep) { if (pLetterStep) *pLetterStep = nullptr; auto* writing = GetWritingData(fontWriting); - if (!writing || !writing->hasCurrentStep || !writing->storage || writing->currentIndex >= writing->storage->characters.size()) return nullptr; + if (!writing || !writing->hasCurrentStep || !writing->hasCurrentCharacter || !writing->storage || writing->currentIndex >= writing->storage->characters.size()) return nullptr; + writing->hasCurrentCharacter = false; auto& letterStep = writing->letterStep; letterStep = {}; letterStep.x = writing->step.x; diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index e0dadb8ba9..2ff7bd7da3 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -3,7 +3,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "prx/libc/include/General.hpp" +#include "prx/libSceFont/include/FontGraphics.hpp" #include "prx/libSceFont/include/FontTypes.hpp" +#include "prx/libSceFont/include/FontWords.hpp" static_assert(sizeof(FontGraphicsRegion) == 32); static_assert(offsetof(FontGraphicsRegion, x) == 0); @@ -81,8 +83,7 @@ int APS5_VABI sceFontCreateGraphicsServiceWithEdition(const FontMemory* memory, } int APS5_VABI sceFontCreateWords(const FontMemory* memory, FontTextSource* source, std::uint64_t unusedSlot, FontWords** words) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::CreateWords(memory, source, words); } int APS5_VABI sceFontDestroyGraphicsDevice(FontGraphicsDevice** device) { @@ -96,24 +97,9 @@ int APS5_VABI sceFontDestroyGraphicsService(FontGraphicsService** service) { } int APS5_VABI sceFontDestroyWords(FontWords** words) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::DestroyWords(words); } -int APS5_VABI sceFontGetFontGlyphsOutlineProfile(FontHandle font, FontOutlineProfile* profile) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGetFontMetrics(FontHandle font, std::uint32_t mode, FontMetrics* metrics) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGetFontStyleInformation(FontHandle font, FontStyleInformation* information) { - NotImplemented_nid_no_patch(__func__); - return 0; -} int APS5_VABI sceFontGetGlyphExpandBufferState(FontHandle font, FontGlyphExpandBufferState* state) { NotImplemented_nid_no_patch(__func__); @@ -161,43 +147,35 @@ int APS5_VABI sceFontGraphicsExchangeResource(FontGraphicsResource* resource, Fo } int APS5_VABI sceFontGraphicsFillMethodInit(FontGraphicsFillMethod* method, std::int32_t mode, std::int32_t type) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsFillMethodInit(method, mode, type); } int APS5_VABI sceFontGraphicsFillPlotInit(FontGraphicsFillPlot* plot, const FontGraphicsRegion* region) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsFillPlotInit(plot, region); } int APS5_VABI sceFontGraphicsFillPlotSetLayout(FontGraphicsFillPlot* plot, std::int32_t first, std::int32_t second) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsFillPlotSetLayout(plot, first, second); } int APS5_VABI sceFontGraphicsFillPlotSetMapping(FontGraphicsFillPlot* plot, float first, float second, float third, float fourth) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsFillPlotSetMapping(plot, first, second, third, fourth); } int APS5_VABI sceFontGraphicsFillRatesInit(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsFillRatesInit(rates, fill, effect); } int APS5_VABI sceFontGraphicsFillRatesSetFillEffect(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsFillRatesSetFillEffect(rates, fill, effect); } int APS5_VABI sceFontGraphicsFillRatesSetLayout(FontGraphicsFillRates* rates, std::int32_t first, std::int32_t second) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsFillRatesSetLayout(rates, first, second); } int APS5_VABI sceFontGraphicsFillRatesSetMapping(FontGraphicsFillRates* rates, float first, float second, float third, float fourth) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsFillRatesSetMapping(rates, first, second, third, fourth); } int APS5_VABI sceFontGraphicsGetDeviceUsage(FontGraphicsDevice* device, FontGraphicsDeviceUsage* usage) { @@ -206,18 +184,15 @@ int APS5_VABI sceFontGraphicsGetDeviceUsage(FontGraphicsDevice* device, FontGrap } int APS5_VABI sceFontGraphicsRegionInit(FontGraphicsRegion* region, float x, float y, float width, float height) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsRegionInit(region, x, y, width, height); } int APS5_VABI sceFontGraphicsRegionInitCircular(FontGraphicsRegion* region, float centerX, float centerY, float outerRadius, float innerRadius) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsRegionInitCircular(region, centerX, centerY, outerRadius, innerRadius); } int APS5_VABI sceFontGraphicsRegionInitRoundish(FontGraphicsRegion* region, float x, float y, float width, float height, float outerRadius, float innerRadius, std::uint32_t flags) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsRegionInitRoundish(region, x, y, width, height, outerRadius, innerRadius, flags); } int APS5_VABI sceFontGraphicsRelease(void** object) { @@ -385,14 +360,17 @@ int APS5_VABI sceFontGraphicsUpdateShapeFillPlot(FontGraphicsResource* resource, return 0; } +void APS5_VABI sceFontSetGraphicsErrorCallback(Font::GraphicsErrorCallback callback) { + Font::SetGraphicsErrorCallback(callback); +} + int APS5_VABI sceFontSupportGlyphs(const FontGlyphSupport* support) { NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceFontWordsFindWordCharacters(FontWords* words, FontTextCharacter* firstCharacter, FontTextCharacter* endCharacter, FontTextCharacter** lastCharacter, FontTextCharacter** nextCharacter) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::WordsFindWordCharacters(words, firstCharacter, endCharacter, lastCharacter, nextCharacter); } int APS5_VABI sceFontGraphicsDrawupFillTextureImageObject(FontGraphicsFrame* frame, const void* data, std::uint32_t dataSize, float x, float y, float width, float height, FontGraphicsFill** fill) { diff --git a/core/libs/prx/libSceFont/src/Words.cpp b/core/libs/prx/libSceFont/src/Words.cpp new file mode 100644 index 0000000000..ae61907c07 --- /dev/null +++ b/core/libs/prx/libSceFont/src/Words.cpp @@ -0,0 +1,296 @@ +#include +#include +#include +#include +#include +#include + +#include "prx/libSceFont/include/Api.hpp" +#include "prx/libSceFont/include/FontDriver.hpp" +#include "prx/libSceFont/include/FontWords.hpp" + +namespace { + +constexpr std::uint16_t WORDS_MAGIC = 0x0F0D; +constexpr std::int32_t PARSER_RESULT_ERROR = -1; +constexpr std::int32_t PARSER_RESULT_TERMINATE = 0; +constexpr std::int32_t PARSER_RESULT_FONT_CODE = 1; +constexpr std::size_t MAX_WORD_DEPTH = 128; +constexpr std::size_t MAX_WORD_NODES = 0xFFFF; +constexpr std::size_t MAX_MATCHES = 16; +constexpr std::uint32_t WORD_END = 0x80000000u; +constexpr std::uint32_t WORD_CODE_MASK = 0x7FFFFFFFu; +constexpr std::uint64_t CHARACTER_SEGMENTED_SCRIPT_MASK = 0xFFull; +constexpr std::uint64_t CHARACTER_BREAK_CLASS_MASK = 0x7ull << 43; +constexpr std::uint64_t CHARACTER_BREAK_CLASS_JOINED = 0x1ull << 43; +constexpr std::uint8_t CLUSTER_KIND_LEADING = 1; + +struct WordNode { + std::uint32_t code; + std::uint16_t childCount; + std::uint16_t firstChild; +}; + +static_assert(sizeof(WordNode) == 8); + +struct WordsData { + std::uint16_t magic = WORDS_MAGIC; + std::uint16_t reserved = 0; + std::uint32_t nodeCount = 0; + std::uint16_t rootCount = 0; + const FontMemory* memory = nullptr; + WordNode* nodes = nullptr; +}; + +struct BuildNode { + std::uint32_t code = 0; + bool end = false; + std::uint16_t childCount = 0; + std::size_t firstChild = 0; +}; + +using BuildLevels = std::vector>; + +struct WordMatch { + FontTextCharacter* last = nullptr; + std::uint32_t length = 0; +}; + +struct MatchRing { + std::array entries{}; + std::uint32_t count = 0; + + const WordMatch& Longest() const { return entries[(count - 1) % MAX_MATCHES]; } +}; + +int ParseFailure(std::int32_t state, const FontTextParseResult& result) { + if (state == PARSER_RESULT_ERROR && result.Error.errorCode < 0) return result.Error.errorCode; + return SCE_FONT_ERROR_INVALID_TEXT_SOURCE; +} + +int AddCode(BuildLevels& levels, std::size_t& depth, std::size_t& pathLength, std::uint32_t code) { + if (levels.size() <= depth) levels.emplace_back(); + auto& level = levels[depth]; + if (depth < pathLength) { + const std::uint32_t previous = level.back().code; + if (code == previous) { + ++depth; + return SCE_FONT_OK; + } + if (code < previous) return SCE_FONT_ERROR_NO_SUPPORT_CODE; + level.push_back({code}); + if (depth > 0) ++levels[depth - 1].back().childCount; + } else { + level.push_back({code}); + if (depth > 0) { + BuildNode& parent = levels[depth - 1].back(); + parent.firstChild = level.size() - 1; + parent.childCount = 1; + } + } + pathLength = ++depth; + return SCE_FONT_OK; +} + +int ReadWords(FontTextSource* source, BuildLevels& levels) { + std::size_t depth = 0; + std::size_t pathLength = 0; + void* order = nullptr; + for (;;) { + FontTextParseResult result{}; + const std::int32_t state = source->textParser(source, &order, &result); + if (state == PARSER_RESULT_TERMINATE) { + if (depth > 0) levels[depth - 1].back().end = true; + if (result.Terminate.terminateCode == 0) return SCE_FONT_OK; + depth = 0; + continue; + } + if (state != PARSER_RESULT_FONT_CODE) return ParseFailure(state, result); + const std::uint32_t code = result.FontCode.code; + if (static_cast(code) <= 0) return SCE_FONT_ERROR_NO_SUPPORT_CODE; + if (depth >= MAX_WORD_DEPTH) return SCE_FONT_OK; + if (const int rc = AddCode(levels, depth, pathLength, code); rc != SCE_FONT_OK) return rc; + } +} + +std::size_t CountNodes(const BuildLevels& levels) { + std::size_t count = 0; + for (const auto& level : levels) count += level.size(); + return count; +} + +void StoreNodes(const BuildLevels& levels, WordNode* nodes) { + std::size_t levelStart = 0; + for (std::size_t depth = 0; depth < levels.size(); ++depth) { + const std::size_t childStart = levelStart + levels[depth].size(); + for (std::size_t i = 0; i < levels[depth].size(); ++i) { + const BuildNode& node = levels[depth][i]; + WordNode& stored = nodes[levelStart + i]; + stored.code = node.code | (node.end ? WORD_END : 0u); + stored.childCount = node.childCount; + stored.firstChild = node.childCount != 0 ? static_cast(childStart + node.firstChild) : 0; + } + levelStart = childStart; + } +} + +int BuildWords(const FontMemory* memory, FontTextSource* source, FontWords** words) { + if (const int rc = sceFontTextSourceRewind(source); rc != SCE_FONT_OK) return rc; + BuildLevels levels; + if (const int rc = ReadWords(source, levels); rc != SCE_FONT_OK) return rc; + const std::size_t nodeCount = CountNodes(levels); + if (nodeCount > MAX_WORD_NODES) return SCE_FONT_ERROR_FUNCTIONAL_LIMIT; + void* raw = memory->iface->alloc(memory->mspace_handle, sizeof(WordsData)); + if (!raw) return SCE_FONT_ERROR_ALLOCATION_FAILED; + auto* data = new (raw) WordsData{}; + auto* nodes = static_cast(memory->iface->alloc(memory->mspace_handle, static_cast(nodeCount * sizeof(WordNode)))); + if (!nodes) { + memory->iface->dealloc(memory->mspace_handle, raw); + return SCE_FONT_ERROR_ALLOCATION_FAILED; + } + StoreNodes(levels, nodes); + data->nodeCount = static_cast(nodeCount); + data->rootCount = levels.empty() ? 0 : static_cast(levels[0].size()); + data->memory = memory; + data->nodes = nodes; + *words = reinterpret_cast(data); + return SCE_FONT_OK; +} + +FontTextCharacter* NextVisible(FontTextCharacter* character) { + do { + character = character->next; + } while (character && character->synthetic != 0); + return character; +} + +FontTextCharacter* NextRendered(FontTextCharacter* character) { + do { + character = character->next; + } while (character && (character->synthetic != 0 || character->clusterIndex != 0)); + return character; +} + +bool InSegmentedRun(const FontTextCharacter* character) { + return (character->flags & CHARACTER_SEGMENTED_SCRIPT_MASK) != 0; +} + +bool EndsWordAfter(const FontTextCharacter* character, const FontTextCharacter* endCharacter) { + const FontTextCharacter* follower = character->next; + if (!follower) return true; + if (follower->synthetic > 0) return false; + if (follower->synthetic < 0 && follower->clusterKind != CLUSTER_KIND_LEADING) return false; + if (follower->synthetic == 0 && follower->clusterIndex != 0) return false; + while (follower && (follower->synthetic != 0 || follower->clusterIndex != 0)) follower = follower->next; + if (!follower || follower == endCharacter) return true; + return (follower->flags & CHARACTER_BREAK_CLASS_MASK) != CHARACTER_BREAK_CLASS_JOINED; +} + +const WordNode* FindCode(const WordNode* group, std::uint32_t size, std::uint32_t code) { + const WordNode* end = group + size; + const WordNode* node = std::lower_bound(group, end, code, [](const WordNode& entry, std::uint32_t value) { + return (entry.code & WORD_CODE_MASK) < value; + }); + return node != end && (node->code & WORD_CODE_MASK) == code ? node : nullptr; +} + +MatchRing MatchWords(const WordsData& data, FontTextCharacter* first, const FontTextCharacter* endCharacter) { + MatchRing ring; + const WordNode* group = data.nodes; + std::uint32_t groupSize = data.rootCount; + FontTextCharacter* character = first; + FontTextCharacter* clusterStart = first; + std::uint32_t length = 1; + while (groupSize != 0) { + const WordNode* node = FindCode(group, groupSize, character->characterCode); + if (!node) break; + if ((node->code & WORD_END) != 0 && EndsWordAfter(character, endCharacter)) { + ring.entries[ring.count % MAX_MATCHES] = {clusterStart, length}; + ++ring.count; + } + if (node->childCount == 0) break; + character = NextVisible(character); + if (!character || character == endCharacter) break; + if (character->clusterIndex == 0) clusterStart = character; + ++length; + group = data.nodes + node->firstChild; + groupSize = node->childCount; + } + return ring; +} + +bool OverrunsNextWord(const WordsData& data, FontTextCharacter* first, const FontTextCharacter* endCharacter, const WordMatch& longest) { + FontTextCharacter* probe = first; + for (std::uint32_t remaining = longest.length; remaining-- > 0;) { + probe = NextVisible(probe); + if (!probe) return false; + if (!InSegmentedRun(probe)) continue; + const MatchRing overlap = MatchWords(data, probe, endCharacter); + return overlap.count != 0 && overlap.Longest().length > remaining; + } + return false; +} + +FontTextCharacter* SelectWord(const WordsData& data, FontTextCharacter* first, const FontTextCharacter* endCharacter) { + const MatchRing matches = MatchWords(data, first, endCharacter); + if (matches.count == 0) return nullptr; + const WordMatch& longest = matches.Longest(); + if (!InSegmentedRun(first) && InSegmentedRun(longest.last) && OverrunsNextWord(data, first, endCharacter, longest)) return nullptr; + const std::uint32_t candidates = std::min(matches.count, MAX_MATCHES); + const WordMatch* fallback = nullptr; + for (std::uint32_t i = 0; i < candidates; ++i) { + const WordMatch& candidate = matches.entries[(matches.count - 1 - i) % MAX_MATCHES]; + if (!candidate.last) break; + FontTextCharacter* follower = NextVisible(candidate.last); + if (!follower || !InSegmentedRun(follower)) return candidate.last; + if (MatchWords(data, follower, endCharacter).count != 0) return candidate.last; + if (!fallback) fallback = &candidate; + } + return fallback ? fallback->last : nullptr; +} + +} + +int Font::CreateWords(const FontMemory* memory, FontTextSource* source, FontWords** words) { + int rc = SCE_FONT_ERROR_INVALID_PARAMETER; + if (memory && (memory->mem_kind != MEMORY_MAGIC || !memory->iface || !memory->iface->alloc || !memory->iface->dealloc)) { + rc = SCE_FONT_ERROR_INVALID_MEMORY; + } else if (memory && source && source->textParser && words) { + rc = BuildWords(memory, source, words); + if (rc == SCE_FONT_OK) { + sceFontTextSourceRewind(source); + return rc; + } + } + if (words) *words = nullptr; + if (source) sceFontTextSourceRewind(source); + return rc; +} + +int Font::DestroyWords(FontWords** words) { + if (!words || !*words) return SCE_FONT_ERROR_INVALID_PARAMETER; + auto* data = reinterpret_cast(*words); + if (data->magic != WORDS_MAGIC || !data->memory || !data->memory->iface || !data->memory->iface->dealloc) return SCE_FONT_ERROR_INVALID_WORDS; + const FontMemory* memory = data->memory; + data->magic = 0; + if (data->nodes) memory->iface->dealloc(memory->mspace_handle, data->nodes); + memory->iface->dealloc(memory->mspace_handle, data); + *words = nullptr; + return SCE_FONT_OK; +} + +int Font::WordsFindWordCharacters(FontWords* words, FontTextCharacter* firstCharacter, FontTextCharacter* endCharacter, FontTextCharacter** lastCharacter, FontTextCharacter** nextCharacter) { + int rc = SCE_FONT_OK; + FontTextCharacter* found = nullptr; + const auto* data = reinterpret_cast(words); + if (!words || !firstCharacter || !lastCharacter) { + rc = SCE_FONT_ERROR_INVALID_PARAMETER; + } else if (data->magic != WORDS_MAGIC || !data->nodes) { + rc = SCE_FONT_ERROR_INVALID_WORDS; + } else { + found = SelectWord(*data, firstCharacter, endCharacter); + } + if (lastCharacter) *lastCharacter = found; + if (nextCharacter) *nextCharacter = found ? NextRendered(found) : nullptr; + return rc; +} diff --git a/core/libs/prx/libSceFontFt/src/Driver.cpp b/core/libs/prx/libSceFontFt/src/Driver.cpp index 3feaa155cf..7ac80488ea 100644 --- a/core/libs/prx/libSceFontFt/src/Driver.cpp +++ b/core/libs/prx/libSceFontFt/src/Driver.cpp @@ -778,6 +778,143 @@ std::uint64_t APS5_VABI RendererQuery(RendererNative* renderer, std::uint8_t* pa return 0; } +float ScaledUnits(std::int64_t units, std::int64_t fixed16) { + return FromF26Dot6(RoundMulFixed(units, fixed16)); +} + +void StoreVerticalFontMetrics(FT_Face face, const TT_OS2* os2, std::int64_t xShift, std::int64_t yShift, FontMetrics* metrics) { + const auto xScale = static_cast(face->size->metrics.x_scale); + const auto yScale = static_cast(face->size->metrics.y_scale); + const auto unitsPerEm = static_cast(static_cast(face->units_per_EM)); + const auto* vhea = static_cast(FT_Get_Sfnt_Table(face, FT_SFNT_VHEA)); + if (vhea) { + const std::int64_t minTop = vhea->min_Top_Side_Bearing; + metrics->ascender = ScaledUnits(minTop + yShift, yScale); + metrics->descender = ScaledUnits(static_cast(vhea->yMax_Extent) + yShift, yScale); + metrics->lineGap = 0.0f; + if (vhea->Version >= 0x110000 && vhea->Line_Gap != 0) { + metrics->lineGap = FromF26Dot6(RoundMulFixed(vhea->Line_Gap, xScale) - TruncFixed(unitsPerEm * xScale)); + } + metrics->verticalDescender = ScaledUnits(static_cast(vhea->Descender) + xShift, xScale); + const std::int64_t ascender = os2 ? os2->sTypoAscender : static_cast(minTop + face->bbox.yMax); + metrics->verticalAscender = ScaledUnits(ascender + yShift, yScale); + } else { + std::int64_t ascender = 0; + if (os2) { + ascender = os2->sTypoAscender; + metrics->ascender = ScaledUnits(ascender - face->bbox.yMax, yScale); + metrics->descender = ScaledUnits(ascender - face->bbox.yMin, yScale); + metrics->lineGap = 0.0f; + } else { + const std::int32_t lineGap = face->height - face->ascender + face->descender; + const std::int32_t halfGap = lineGap / 2; + metrics->ascender = ScaledUnits(halfGap, yScale); + metrics->descender = ScaledUnits(face->ascender - face->descender + halfGap, yScale); + metrics->lineGap = ScaledUnits(lineGap, xScale); + ascender = face->ascender + halfGap; + } + metrics->verticalAscender = ScaledUnits(ascender + yShift, yScale); + metrics->verticalDescender = ScaledUnits(-static_cast(unitsPerEm / 2), xScale); + } + metrics->maxAdvance = ScaledUnits(face->max_advance_height, yScale); +} + +std::uint32_t StoreHorizontalFontMetrics(FT_Face face, const TT_OS2* os2, std::uint32_t form, std::int64_t xShift, std::int64_t yShift, FontMetrics* metrics) { + const auto xScale = static_cast(face->size->metrics.x_scale); + const auto yScale = static_cast(face->size->metrics.y_scale); + const auto* hhea = static_cast(FT_Get_Sfnt_Table(face, FT_SFNT_HHEA)); + metrics->verticalDescender = 0.0f; + metrics->verticalAscender = 0.0f; + bool fromHorizontalHeader = form == 1 || ((form == 2 || form == 3) && !os2); + if (form == 2 && os2) { + metrics->ascender = ScaledUnits(static_cast(os2->sTypoAscender) + yShift, yScale); + metrics->descender = ScaledUnits(static_cast(os2->sTypoDescender) + yShift, yScale); + metrics->lineGap = ScaledUnits(os2->sTypoLineGap, yScale); + fromHorizontalHeader = metrics->ascender == 0.0f && metrics->descender == 0.0f; + } else if (form == 3 && os2) { + metrics->ascender = ScaledUnits(static_cast(os2->usWinAscent) + yShift, yScale); + metrics->descender = FromF26Dot6(-RoundMulFixed(static_cast(os2->usWinDescent) - yShift, yScale)); + metrics->lineGap = hhea ? ScaledUnits(hhea->Line_Gap, yScale) : 0.0f; + fromHorizontalHeader = metrics->ascender == metrics->descender; + } + if (fromHorizontalHeader) { + form = 1; + if (hhea) { + metrics->ascender = ScaledUnits(static_cast(hhea->Ascender) + yShift, yScale); + metrics->descender = ScaledUnits(static_cast(hhea->Descender) + yShift, yScale); + metrics->lineGap = ScaledUnits(hhea->Line_Gap, yScale); + } else { + metrics->ascender = ScaledUnits(static_cast(face->ascender) + yShift, yScale); + metrics->descender = ScaledUnits(static_cast(face->descender) + yShift, yScale); + metrics->lineGap = 0.0f; + } + } + metrics->maxAdvance = ScaledUnits(static_cast(face->max_advance_width) + xShift, xScale); + return form; +} + +int APS5_VABI LibraryGetFontMetrics(FontObj* obj, std::uint32_t form, FontMetrics* metrics) { + const FT_Face face = obj ? FaceOf(obj) : nullptr; + if (!face || !face->size || !metrics) return SCE_FONT_ERROR_FATAL; + const auto xScale = static_cast(face->size->metrics.x_scale); + const auto yScale = static_cast(face->size->metrics.y_scale); + const std::int64_t xShift = obj->shift_cache_x; + const std::int64_t yShift = obj->shift_cache_y; + const auto* os2 = static_cast(FT_Get_Sfnt_Table(face, FT_SFNT_OS2)); + if (os2 && os2->version == 0xFFFF) os2 = nullptr; + if (form == 4) { + StoreVerticalFontMetrics(face, os2, xShift, yShift, metrics); + } else { + form = StoreHorizontalFontMetrics(face, os2, form, xShift, yShift, metrics); + } + metrics->xMin = ScaledUnits(static_cast(face->bbox.xMin) + xShift, xScale); + metrics->yMin = ScaledUnits(static_cast(face->bbox.yMin) + yShift, yScale); + metrics->xMax = ScaledUnits(static_cast(face->bbox.xMax) + xShift, xScale); + metrics->yMax = ScaledUnits(static_cast(face->bbox.yMax) + yShift, yScale); + const auto unitsPerEm = static_cast(static_cast(face->units_per_EM)); + metrics->emWidth = FromF26Dot6(TruncFixed(unitsPerEm * xScale)); + metrics->emHeight = FromF26Dot6(TruncFixed(unitsPerEm * yScale)); + const auto* handle = reinterpret_cast(obj->font_handle); + metrics->effectWeightX = handle ? handle->style.effect_weight_x : 0.0f; + metrics->effectWeightY = handle ? handle->style.effect_weight_y : 0.0f; + metrics->slantRatio = handle ? handle->style.slant_ratio : 0.0f; + metrics->form = form; + return SCE_FONT_OK; +} + +int APS5_VABI LibraryGetOutlineProfile(FontObj* obj, FontOutlineProfile* profile) { + const FT_Face face = obj ? FaceOf(obj) : nullptr; + if (!face) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + const auto* maxp = static_cast(FT_Get_Sfnt_Table(face, FT_SFNT_MAXP)); + if (!maxp) throw std::runtime_error("FontFt: outline profile of a face without a maxp table"); + if (maxp->version < 0x10000) return SCE_FONT_ERROR_NO_SUPPORT_FUNCTION; + profile->maxContours = maxp->maxContours; + profile->maxPoints = maxp->maxPoints; + profile->maxCompositeContours = maxp->maxCompositeContours; + profile->maxCompositePoints = maxp->maxCompositePoints; + return SCE_FONT_OK; +} + +std::uint16_t WeightClass(std::uint16_t weight) { + return weight >= 100 && weight <= 900 && weight % 100 == 0 ? static_cast(weight / 100) : 0; +} + +int APS5_VABI LibraryGetFontStyleInformation(FontObj* obj, FontStyleInformation* information) { + const FT_Face face = obj ? FaceOf(obj) : nullptr; + if (!face || !information) return SCE_FONT_ERROR_FATAL; + const auto* header = static_cast(FT_Get_Sfnt_Table(face, FT_SFNT_HEAD)); + if (!header) throw std::runtime_error("FontFt: style information of a face without a head table"); + const auto* os2 = static_cast(FT_Get_Sfnt_Table(face, FT_SFNT_OS2)); + if (os2 && os2->version == 0xFFFF) os2 = nullptr; + information->familyName = face->family_name; + information->styleName = face->style_name; + information->styleFlags = static_cast(face->style_flags & (FT_STYLE_FLAG_ITALIC | FT_STYLE_FLAG_BOLD)); + information->weight = os2 ? WeightClass(os2->usWeightClass) : 0; + information->width = os2 && os2->usWidthClass < 10 ? os2->usWidthClass : 0; + information->layoutFlags = static_cast(1 + (header->Font_Direction < 0 ? 1 : 0) + (FT_HAS_VERTICAL(face) ? 0x80 : 0)); + return SCE_FONT_OK; +} + SysDriver MakeDriverTable() { SysDriver driver{}; driver.pixel_resolution = &LibraryGetPixelResolution; @@ -792,6 +929,9 @@ SysDriver MakeDriverTable() { driver.glyph_index = &LibraryGetGlyphIndex; driver.set_char_with_dpi = &LibrarySetCharSizeWithDpi; driver.set_char_default_dpi = &LibrarySetCharSizeDefaultDpi; + driver.font_metrics = &LibraryGetFontMetrics; + driver.style_information = &LibraryGetFontStyleInformation; + driver.outline_profile = &LibraryGetOutlineProfile; driver.compute_layout = &LibraryComputeLayout; driver.compute_layout_alt = &LibraryComputeLayoutAlt; driver.load_glyph_cached = &LibraryLoadGlyphCached; diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index 4fec867345..fb0affb25c 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -7,6 +7,7 @@ add_library(libSceFreeType SHARED EXCLUDE_FROM_ALL src/GuestObjects.cpp src/Names.cpp src/Trigonometry.cpp + src/OutlineDecompose.cpp ) target_include_directories(libSceFreeType PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceFreeType PRIVATE freetype) diff --git a/core/libs/prx/libSceFreeType/Export.cpp b/core/libs/prx/libSceFreeType/Export.cpp index 0ff5071b79..a8919bfb49 100644 --- a/core/libs/prx/libSceFreeType/Export.cpp +++ b/core/libs/prx/libSceFreeType/Export.cpp @@ -6,6 +6,7 @@ #include FT_GASP_H #include FT_CID_H +#include FT_SIZES_H #include #include @@ -33,15 +34,19 @@ void FinalizeSlot(void* object) { void FinalizeSize(void* object) { auto* host = static_cast(object); - auto* face = static_cast(host->generic.data); - if (face->size.generic.finalizer) face->size.generic.finalizer(&face->size); - face->rec.size = nullptr; + auto* rec = static_cast(host->generic.data); + auto* face = static_cast(host->face->generic.data); + SyncSizeRecord_nid_no_patch(*face, *rec, host); + SyncSizePointer_nid_no_patch(*face); + if (rec->generic.finalizer) rec->generic.finalizer(rec); } void FinalizeFace(void* object) { auto* host = static_cast(object); auto* face = static_cast(host->generic.data); + SyncSizePointer_nid_no_patch(*face); if (face->rec.generic.finalizer) face->rec.generic.finalizer(&face->rec); + face->finalized = true; } void PublishFace(GuestLibrary* library, std::unique_ptr face, FT_Face host, GuestFaceRec** aface) { @@ -53,7 +58,7 @@ void PublishFace(GuestLibrary* library, std::unique_ptr face, FT_Face library->faces.push_back(std::move(face)); host->generic = {owner, FinalizeFace}; if (host->glyph) host->glyph->generic = {owner, FinalizeSlot}; - if (host->size) host->size->generic = {owner, FinalizeSize}; + if (host->size) host->size->generic = {&owner->size, FinalizeSize}; *aface = &owner->rec; hostOwner.release(); } @@ -114,6 +119,13 @@ int APS5_VABI FT_Done_FreeType_nid_postfix(GuestLibrary* library) { return error; } +int APS5_VABI FT_Select_Size_nid_postfix(GuestFaceRec* face, std::int32_t strikeIndex) { + GuestFace* owner = FaceOf(face); + const FT_Error error = FT_Select_Size(owner ? owner->host : nullptr, strikeIndex); + if (owner) SyncSize(*owner); + return error; +} + int APS5_VABI FT_New_Face_nid_postfix(GuestLibrary* library, const char* filepathname, std::int64_t face_index, GuestFaceRec** aface) { if (!filepathname) return FT_Err_Invalid_Argument; const std::int64_t faceIndex = OpenFaceIndex(face_index); @@ -144,13 +156,72 @@ int APS5_VABI FT_New_Memory_Face_nid_postfix(GuestLibrary* library, const unsign return 0; } +int APS5_VABI FT_Reference_Face_nid_postfix(GuestFaceRec* face) { + const GuestFace* owner = FaceOf(face); + return FT_Reference_Face(owner ? owner->host : nullptr); +} + int APS5_VABI FT_Done_Face_nid_postfix(GuestFaceRec* face) { GuestFace* owner = FaceOf(face); const FT_Error error = FT_Done_Face(owner ? owner->host : nullptr); - if (error == 0) owner->library->faces.remove_if([owner](const auto& entry) { return entry.get() == owner; }); + if (error == 0 && owner->finalized) owner->library->faces.remove_if([owner](const auto& entry) { return entry.get() == owner; }); return error; } +int APS5_VABI FT_New_Size_nid_postfix(GuestFaceRec* face, GuestSizeRec** asize) { + GuestFace* owner = FaceOf(face); + FT_Face hostFace = owner ? owner->host : nullptr; + if (!hostFace || !asize || !hostFace->driver) { + FT_Size unused = nullptr; + return FT_New_Size(hostFace, asize ? &unused : nullptr); + } + const bool activates = asize == &owner->rec.size; + *asize = nullptr; + if (activates) hostFace->size = nullptr; + try { + owner->sizes.emplace_back(); + } catch (const std::bad_alloc&) { + return FT_Err_Out_Of_Memory; + } + auto entry = std::prev(owner->sizes.end()); + FT_Size host = nullptr; + const FT_Error error = FT_New_Size(hostFace, activates ? &hostFace->size : &host); + if (activates) host = hostFace->size; + if (error != 0) { + owner->sizes.erase(entry); + SyncSizePointer_nid_no_patch(*owner); + return error; + } + entry->host = host; + SyncSizeRecord_nid_no_patch(*owner, entry->rec, host); + host->generic = {&entry->rec, FinalizeSize}; + *asize = &entry->rec; + SyncSizePointer_nid_no_patch(*owner); + return 0; +} + +int APS5_VABI FT_Activate_Size_nid_postfix(GuestSizeRec* size) { + FT_Size host = size ? static_cast(size->internal) : nullptr; + const FT_Error error = FT_Activate_Size(host); + if (error == 0) SyncSize(*static_cast(host->face->generic.data)); + return error; +} + +int APS5_VABI FT_Done_Size_nid_postfix(GuestSizeRec* size) { + FT_Size host = size ? static_cast(size->internal) : nullptr; + auto* owner = host && host->face ? static_cast(host->face->generic.data) : nullptr; + const FT_Error error = FT_Done_Size(host); + if (error != 0) return error; + if (host == owner->initialSize) { + owner->initialSize = nullptr; + owner->size.internal = nullptr; + } else { + owner->sizes.remove_if([host](const auto& entry) { return entry.host == host; }); + } + SyncSize(*owner); + return 0; +} + int APS5_VABI FT_Set_Pixel_Sizes_nid_postfix(GuestFaceRec* face, std::uint32_t pixel_width, std::uint32_t pixel_height) { GuestFace* owner = FaceOf(face); const FT_Error error = FT_Set_Pixel_Sizes(owner ? owner->host : nullptr, pixel_width, pixel_height); @@ -277,6 +348,19 @@ int APS5_VABI FT_Render_Glyph_nid_postfix(GuestGlyphSlotRec* slot, std::int32_t return error; } +int APS5_VABI FT_Outline_Copy_nid_postfix(const GuestOutline* source, GuestOutline* target) { + if (!source || !target) return FT_Err_Invalid_Outline; + if (source->n_points != target->n_points || source->n_contours != target->n_contours) return FT_Err_Invalid_Argument; + if (source == target) return FT_Err_Ok; + if (source->n_points) { + std::memmove(target->points, source->points, source->n_points * sizeof(GuestVector)); + std::memmove(target->tags, source->tags, source->n_points * sizeof(unsigned char)); + } + if (source->n_contours) std::memmove(target->contours, source->contours, source->n_contours * sizeof(std::uint16_t)); + target->flags = (source->flags & ~FT_OUTLINE_OWNER) | (target->flags & FT_OUTLINE_OWNER); + return FT_Err_Ok; +} + int APS5_VABI FT_Select_Charmap_nid_postfix(GuestFaceRec* face, std::int32_t encoding) { GuestFace* owner = FaceOf(face); const FT_Error error = FT_Select_Charmap(owner ? owner->host : nullptr, static_cast(encoding)); diff --git a/core/libs/prx/libSceFreeType/include/Api.hpp b/core/libs/prx/libSceFreeType/include/Api.hpp index 7c2f791458..926be2864a 100644 --- a/core/libs/prx/libSceFreeType/include/Api.hpp +++ b/core/libs/prx/libSceFreeType/include/Api.hpp @@ -6,15 +6,26 @@ extern "C" { +std::int32_t APS5_VABI FT_Outline_GetInsideBorder_nid_postfix(const FreeType::GuestOutline* outline); +std::int32_t APS5_VABI FT_Outline_GetOutsideBorder_nid_postfix(const FreeType::GuestOutline* outline); + int APS5_VABI FT_Init_FreeType_nid_postfix(FreeType::GuestLibrary** alibrary); int APS5_VABI FT_Done_FreeType_nid_postfix(FreeType::GuestLibrary* library); int APS5_VABI FT_New_Face_nid_postfix(FreeType::GuestLibrary* library, const char* filepathname, std::int64_t faceIndex, FreeType::GuestFaceRec** aface); int APS5_VABI FT_New_Memory_Face_nid_postfix(FreeType::GuestLibrary* library, const unsigned char* fileBase, std::int64_t fileSize, std::int64_t faceIndex, FreeType::GuestFaceRec** aface); +int APS5_VABI FT_Reference_Face_nid_postfix(FreeType::GuestFaceRec* face); int APS5_VABI FT_Done_Face_nid_postfix(FreeType::GuestFaceRec* face); +int APS5_VABI FT_Select_Size_nid_postfix(FreeType::GuestFaceRec* face, std::int32_t strikeIndex); +int APS5_VABI FT_New_Size_nid_postfix(FreeType::GuestFaceRec* face, FreeType::GuestSizeRec** asize); +int APS5_VABI FT_Activate_Size_nid_postfix(FreeType::GuestSizeRec* size); +int APS5_VABI FT_Done_Size_nid_postfix(FreeType::GuestSizeRec* size); int APS5_VABI FT_Set_Pixel_Sizes_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t pixelWidth, std::uint32_t pixelHeight); int APS5_VABI FT_Set_Char_Size_nid_postfix(FreeType::GuestFaceRec* face, std::int64_t charWidth, std::int64_t charHeight, std::uint32_t horizontalResolution, std::uint32_t verticalResolution); std::uint32_t APS5_VABI FT_Get_Char_Index_nid_postfix(FreeType::GuestFaceRec* face, std::uint64_t charcode); int APS5_VABI FT_Get_Kerning_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t leftGlyph, std::uint32_t rightGlyph, std::uint32_t kernMode, FreeType::GuestVector* kerning); +int APS5_VABI FT_Get_PFR_Advance_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t index, std::int64_t* advance); +int APS5_VABI FT_Get_PFR_Kerning_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t left, std::uint32_t right, FreeType::GuestVector* vector); +int APS5_VABI FT_Get_PFR_Metrics_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t* outline, std::uint32_t* metrics, std::int64_t* xScale, std::int64_t* yScale); int APS5_VABI FT_Load_Glyph_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t glyphIndex, std::int32_t loadFlags); int APS5_VABI FT_Get_Gasp_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t ppem); int APS5_VABI FT_Get_CID_Registry_Ordering_Supplement_nid_postfix(FreeType::GuestFaceRec* face, const char** registry, const char** ordering, std::int32_t* supplement); @@ -22,6 +33,8 @@ int APS5_VABI FT_Get_CID_Is_Internally_CID_Keyed_nid_postfix(FreeType::GuestFace int APS5_VABI FT_Get_CID_From_Glyph_Index_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t glyphIndex, std::uint32_t* cid); int APS5_VABI FT_Load_Char_nid_postfix(FreeType::GuestFaceRec* face, std::uint64_t charCode, std::int32_t loadFlags); int APS5_VABI FT_Outline_Get_Orientation_nid_postfix(FreeType::GuestOutline* outline); +int APS5_VABI FT_Outline_Copy_nid_postfix(const FreeType::GuestOutline* source, FreeType::GuestOutline* target); +int APS5_VABI FT_Outline_Decompose_nid_postfix(FreeType::GuestOutline* outline, const FreeType::GuestOutlineFuncs* functions, void* user); int APS5_VABI FT_Render_Glyph_nid_postfix(FreeType::GuestGlyphSlotRec* slot, std::int32_t renderMode); int APS5_VABI FT_Select_Charmap_nid_postfix(FreeType::GuestFaceRec* face, std::int32_t encoding); int APS5_VABI FT_Set_Charmap_nid_postfix(FreeType::GuestFaceRec* face, FreeType::GuestCharMapRec* charmap); diff --git a/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp index 20553508a0..51acae2148 100644 --- a/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp +++ b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp @@ -115,6 +115,15 @@ struct GuestOutline { std::int32_t flags; }; +struct GuestOutlineFuncs { + int (APS5_VABI *move_to)(const GuestVector*, void*); + int (APS5_VABI *line_to)(const GuestVector*, void*); + int (APS5_VABI *conic_to)(const GuestVector*, const GuestVector*, void*); + int (APS5_VABI *cubic_to)(const GuestVector*, const GuestVector*, const GuestVector*, void*); + std::int32_t shift; + std::int64_t delta; +}; + struct GuestGlyphSlotRec { void* library; void* face; @@ -185,6 +194,7 @@ static_assert(sizeof(GuestSizeRec) == 88 && offsetof(GuestSizeRec, metrics) == 2 static_assert(sizeof(GuestSizeRequestRec) == 32 && offsetof(GuestSizeRequestRec, width) == 8 && offsetof(GuestSizeRequestRec, height) == 16); static_assert(offsetof(GuestSizeRequestRec, horiResolution) == 24 && offsetof(GuestSizeRequestRec, vertResolution) == 28); static_assert(sizeof(GuestBitmap) == 40 && offsetof(GuestBitmap, buffer) == 16 && offsetof(GuestBitmap, palette) == 32); +static_assert(sizeof(GuestOutlineFuncs) == 48 && offsetof(GuestOutlineFuncs, shift) == 32 && offsetof(GuestOutlineFuncs, delta) == 40); static_assert(sizeof(GuestOutline) == 40 && offsetof(GuestOutline, points) == 8 && offsetof(GuestOutline, flags) == 32); static_assert(offsetof(GuestGlyphSlotRec, glyph_index) == 0x18 && offsetof(GuestGlyphSlotRec, metrics) == 0x30); static_assert(offsetof(GuestGlyphSlotRec, advance) == 0x80 && offsetof(GuestGlyphSlotRec, format) == 0x90); diff --git a/core/libs/prx/libSceFreeType/include/GuestList.hpp b/core/libs/prx/libSceFreeType/include/GuestList.hpp index 67a8396b87..1e11a349c1 100644 --- a/core/libs/prx/libSceFreeType/include/GuestList.hpp +++ b/core/libs/prx/libSceFreeType/include/GuestList.hpp @@ -3,16 +3,21 @@ #include "prx/libc/include/general/VabiMacros.hpp" -#include -#include FT_LIST_H - #include #include namespace FreeType { -using GuestListRec = FT_ListRec; -using GuestListNodeRec = FT_ListNodeRec; +struct GuestListNodeRec { + GuestListNodeRec* prev; + GuestListNodeRec* next; + void* data; +}; + +struct GuestListRec { + GuestListNodeRec* head; + GuestListNodeRec* tail; +}; struct GuestMemoryRec { void* user; diff --git a/core/libs/prx/libSceFreeType/include/GuestObjects.hpp b/core/libs/prx/libSceFreeType/include/GuestObjects.hpp index 9ae0c12315..d47c3c1037 100644 --- a/core/libs/prx/libSceFreeType/include/GuestObjects.hpp +++ b/core/libs/prx/libSceFreeType/include/GuestObjects.hpp @@ -21,12 +21,21 @@ struct GuestLibrary { void UnregisterBitmap_nid_no_patch(const GuestBitmap* bitmap); +struct GuestSize { + GuestSizeRec rec{}; + FT_Size host = nullptr; +}; + struct GuestFace { GuestFaceRec rec{}; GuestGlyphSlotRec slot{}; GuestSizeRec size{}; GuestLibrary* library = nullptr; FT_Face host = nullptr; + bool finalized = false; + FT_Size initialSize = nullptr; + bool sizeInitialized = false; + std::list sizes; std::vector bytes; std::vector fixedSizes; std::vector charmaps; @@ -42,6 +51,8 @@ GuestFace* FaceOf(const GuestFaceRec* face); GuestFace* FaceOf(const GuestGlyphSlotRec* slot); void SyncFace(GuestFace& face); void SyncSize(GuestFace& face); +void SyncSizePointer_nid_no_patch(GuestFace& face); +void SyncSizeRecord_nid_no_patch(GuestFace& face, GuestSizeRec& rec, FT_Size host); void SyncSlot(GuestFace& face); } diff --git a/core/libs/prx/libSceFreeType/include/GuestPfr.hpp b/core/libs/prx/libSceFreeType/include/GuestPfr.hpp new file mode 100644 index 0000000000..deb4468530 --- /dev/null +++ b/core/libs/prx/libSceFreeType/include/GuestPfr.hpp @@ -0,0 +1,14 @@ +#ifndef CORE_LIBS_PRX_LIBSCEFREETYPE_INCLUDE_GUESTPFR_HPP +#define CORE_LIBS_PRX_LIBSCEFREETYPE_INCLUDE_GUESTPFR_HPP + +#include "prx/libSceFreeType/include/GuestFreeType.hpp" + +extern "C" { + +int FreeTypePfrAdvance_nid_no_patch(FreeType::GuestFaceRec* face, std::uint32_t index, std::int64_t* advance); +int FreeTypePfrKerning_nid_no_patch(FreeType::GuestFaceRec* face, std::uint32_t left, std::uint32_t right, FreeType::GuestVector* vector); +int FreeTypePfrMetrics_nid_no_patch(FreeType::GuestFaceRec* face, std::uint32_t* outline, std::uint32_t* metrics, std::int64_t* xScale, std::int64_t* yScale); + +} + +#endif diff --git a/core/libs/prx/libSceFreeType/src/GuestObjects.cpp b/core/libs/prx/libSceFreeType/src/GuestObjects.cpp index 17d1888a21..1ecb345092 100644 --- a/core/libs/prx/libSceFreeType/src/GuestObjects.cpp +++ b/core/libs/prx/libSceFreeType/src/GuestObjects.cpp @@ -1,6 +1,7 @@ #include "prx/libSceFreeType/include/GuestObjects.hpp" #include +#include namespace FreeType { @@ -51,20 +52,48 @@ void SyncFace(GuestFace& face) { rec.underline_position = host->underline_position; rec.underline_thickness = host->underline_thickness; rec.glyph = host->glyph ? &face.slot : nullptr; - rec.size = host->size ? &face.size : nullptr; rec.internal = &face; SyncSize(face); SyncSlot(face); } -void SyncSize(GuestFace& face) { - face.size.face = &face.rec; - if (!face.host->size) { - face.size.metrics = {}; +void SyncSizeRecord_nid_no_patch(GuestFace& face, GuestSizeRec& rec, FT_Size host) { + rec.face = &face.rec; + rec.internal = host; + if (!host) { + rec.metrics = {}; return; } - const auto& metrics = face.host->size->metrics; - face.size.metrics = {metrics.x_ppem, metrics.y_ppem, metrics.x_scale, metrics.y_scale, metrics.ascender, metrics.descender, metrics.height, metrics.max_advance}; + const auto& metrics = host->metrics; + rec.metrics = {metrics.x_ppem, metrics.y_ppem, metrics.x_scale, metrics.y_scale, metrics.ascender, metrics.descender, metrics.height, metrics.max_advance}; +} + +void SyncSizePointer_nid_no_patch(GuestFace& face) { + const FT_Size active = face.host->size; + if (!active) { + face.rec.size = nullptr; + return; + } + if (active == face.initialSize) { + face.rec.size = &face.size; + return; + } + for (auto& size : face.sizes) { + if (size.host != active) continue; + face.rec.size = &size.rec; + return; + } + throw std::runtime_error("FreeType: an active size created outside the guest bridge is not implemented"); +} + +void SyncSize(GuestFace& face) { + if (!face.sizeInitialized) { + face.initialSize = face.host->size; + face.sizeInitialized = true; + SyncSizeRecord_nid_no_patch(face, face.size, face.initialSize); + } + SyncSizePointer_nid_no_patch(face); + if (face.rec.size) SyncSizeRecord_nid_no_patch(face, *face.rec.size, face.host->size); } void SyncSlot(GuestFace& face) { diff --git a/core/libs/prx/libSceFreeType/src/List.cpp b/core/libs/prx/libSceFreeType/src/List.cpp index 45a1872a31..198f3207b6 100644 --- a/core/libs/prx/libSceFreeType/src/List.cpp +++ b/core/libs/prx/libSceFreeType/src/List.cpp @@ -4,6 +4,8 @@ #include #include +#include +#include FT_LIST_H using namespace FreeType; @@ -26,77 +28,89 @@ private: std::array writes; }; -struct Iteration { - GuestListIterator iterator; - void* user; -}; - -int Iterate(FT_ListNode node, void* user) { - auto& call = *static_cast(user); - return call.iterator(node, call.user); -} - -struct Finalization { - GuestMemoryRec* memory; - GuestListDestructor destroy; - void* user; -}; - -void Destroy(FT_Memory memory, void* data, void*) { - auto& call = *static_cast(memory->user); - call.destroy(call.memory, data, call.user); -} - -void FreeNode(FT_Memory memory, void* block) { - auto& call = *static_cast(memory->user); - call.memory->free(call.memory, block); -} - } extern "C" { GuestListNodeRec* APS5_VABI FT_List_Find_nid_postfix(GuestListRec* list, void* data) { - return FT_List_Find(list, data); + if (!list) return nullptr; + for (auto* node = list->head; node; node = node->next) { + if (node->data == data) return node; + } + return nullptr; } void APS5_VABI FT_List_Add_nid_postfix(GuestListRec* list, GuestListNodeRec* node) { if (!list || !node) return; const ListWrite write(list, {node, list->tail}); - FT_List_Add(list, node); + auto* tail = list->tail; + node->next = nullptr; + node->prev = tail; + if (tail) tail->next = node; + else list->head = node; + list->tail = node; } void APS5_VABI FT_List_Insert_nid_postfix(GuestListRec* list, GuestListNodeRec* node) { if (!list || !node) return; const ListWrite write(list, {node, list->head}); - FT_List_Insert(list, node); + auto* head = list->head; + node->next = head; + node->prev = nullptr; + if (head) head->prev = node; + else list->tail = node; + list->head = node; } void APS5_VABI FT_List_Remove_nid_postfix(GuestListRec* list, GuestListNodeRec* node) { if (!list || !node) return; const ListWrite write(list, {node->prev, node->next}); - FT_List_Remove(list, node); + auto* previous = node->prev; + auto* next = node->next; + if (previous) previous->next = next; + else list->head = next; + if (next) next->prev = previous; + else list->tail = previous; } void APS5_VABI FT_List_Up_nid_postfix(GuestListRec* list, GuestListNodeRec* node) { if (!list || !node || !node->prev) return; const ListWrite write(list, {node, node->prev, node->next, list->head}); - FT_List_Up(list, node); + auto* previous = node->prev; + auto* next = node->next; + previous->next = next; + if (next) next->prev = previous; + else list->tail = previous; + node->prev = nullptr; + node->next = list->head; + list->head->prev = node; + list->head = node; } int APS5_VABI FT_List_Iterate_nid_postfix(GuestListRec* list, GuestListIterator iterator, void* user) { - Iteration call{iterator, user}; - return FT_List_Iterate(list, iterator ? Iterate : nullptr, &call); + if (!list || !iterator) return FT_Err_Invalid_Argument; + auto* node = list->head; + while (node) { + auto* next = node->next; + const int error = iterator(node, user); + if (error) return error; + node = next; + } + return 0; } void APS5_VABI FT_List_Finalize_nid_postfix(GuestListRec* list, GuestListDestructor destroy, GuestMemoryRec* memory, void* user) { if (!list || !memory) return; const ListWrite write(list); - Finalization call{memory, destroy, user}; - FT_MemoryRec_ native{}; - native.user = &call; - native.free = FreeNode; - FT_List_Finalize(list, destroy ? Destroy : nullptr, &native, user); + auto* node = list->head; + while (node) { + auto* next = node->next; + if (destroy) destroy(memory, node->data, user); + memory->free(memory, node); + node = next; + } + list->head = nullptr; + list->tail = nullptr; } } diff --git a/core/libs/prx/libSceFreeType/src/OutlineBorders.cpp b/core/libs/prx/libSceFreeType/src/OutlineBorders.cpp new file mode 100644 index 0000000000..34d54d7c03 --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/OutlineBorders.cpp @@ -0,0 +1,68 @@ +#include "prx/libSceFreeType/include/Api.hpp" +#include +#include +#include +#include + +namespace { + +bool Clockwise(const FreeType::GuestOutline* outline) { + if (!outline || outline->n_points == 0) return true; + if (!outline->points) throw std::invalid_argument("FreeType outline border: null points"); + auto minimum = outline->points[0]; + auto maximum = minimum; + for (unsigned i = 1; i < outline->n_points; ++i) { + const auto point = outline->points[i]; + minimum.x = std::min(minimum.x, point.x); + minimum.y = std::min(minimum.y, point.y); + maximum.x = std::max(maximum.x, point.x); + maximum.y = std::max(maximum.y, point.y); + } + if (minimum.x == maximum.x || minimum.y == maximum.y) return false; + constexpr std::int64_t limit = 0x1000000; + if (minimum.x < -limit || minimum.y < -limit || maximum.x > limit || maximum.y > limit) return false; + if (outline->n_contours == 0) return false; + if (!outline->contours) throw std::invalid_argument("FreeType outline border: null contours"); + unsigned first = 0; + for (unsigned contour = 0; contour < outline->n_contours; ++contour) { + const unsigned last = outline->contours[contour]; + if (last < first || last >= outline->n_points) + throw std::invalid_argument("FreeType outline border: invalid contour endpoint"); + first = last + 1; + } + const auto magnitude = [](std::int64_t value) { return static_cast(value < 0 ? -value : value); }; + const auto shift = [](std::uint32_t value) { const auto bits = std::bit_width(value); return bits > 15 ? bits - 15 : 0; }; + const auto xShift = shift(magnitude(maximum.x) | magnitude(minimum.x)); + const auto yShift = shift(static_cast(maximum.y - minimum.y)); + std::int64_t area = 0; + first = 0; + for (unsigned contour = 0; contour < outline->n_contours; ++contour) { + const unsigned last = outline->contours[contour]; + auto previous = outline->points[last]; + previous.x >>= xShift; + previous.y >>= yShift; + for (unsigned i = first; i <= last; ++i) { + auto current = outline->points[i]; + current.x >>= xShift; + current.y >>= yShift; + area += (current.y - previous.y) * (current.x + previous.x); + previous = current; + } + first = last + 1; + } + return area < 0; +} + +} + +extern "C" { + +std::int32_t APS5_VABI FT_Outline_GetInsideBorder_nid_postfix(const FreeType::GuestOutline* outline) { + return Clockwise(outline) ? 1 : 0; +} + +std::int32_t APS5_VABI FT_Outline_GetOutsideBorder_nid_postfix(const FreeType::GuestOutline* outline) { + return Clockwise(outline) ? 0 : 1; +} + +} diff --git a/core/libs/prx/libSceFreeType/src/OutlineDecompose.cpp b/core/libs/prx/libSceFreeType/src/OutlineDecompose.cpp new file mode 100644 index 0000000000..2304dc5a9b --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/OutlineDecompose.cpp @@ -0,0 +1,130 @@ +#include "prx/libSceFreeType/include/GuestFreeType.hpp" + +#include +#include FT_FREETYPE_H + +#include +#include +#include + +using namespace FreeType; + +namespace { + +std::int64_t Scale(std::int64_t value, std::int32_t shift, std::int64_t delta) { + if (shift < 0 || shift >= 63) { + throw std::runtime_error("FT_Outline_Decompose: a shift outside 0-62 is not implemented"); + } + constexpr auto minimum = std::numeric_limits::min(); + constexpr auto maximum = std::numeric_limits::max(); + const std::int64_t factor = std::int64_t{1} << shift; + if (value < minimum / factor || value > maximum / factor) { + throw std::runtime_error("FT_Outline_Decompose: overflowing coordinate scaling is not implemented"); + } + const std::int64_t scaled = value * factor; + if ((delta > 0 && scaled < minimum + delta) || (delta < 0 && scaled > maximum + delta)) { + throw std::runtime_error("FT_Outline_Decompose: overflowing coordinate translation is not implemented"); + } + return scaled - delta; +} + +GuestVector Scale(GuestVector point, std::int32_t shift, std::int64_t delta) { + return {Scale(point.x, shift, delta), Scale(point.y, shift, delta)}; +} + +std::int64_t Midpoint(std::int64_t first, std::int64_t second) { + if ((second > 0 && first > std::numeric_limits::max() - second) || + (second < 0 && first < std::numeric_limits::min() - second)) { + throw std::runtime_error("FT_Outline_Decompose: overflowing midpoint arithmetic is not implemented"); + } + return (first + second) / 2; +} + +GuestVector Midpoint(GuestVector first, GuestVector second) { + return {Midpoint(first.x, second.x), Midpoint(first.y, second.y)}; +} + +} + +extern "C" int APS5_VABI FT_Outline_Decompose_nid_postfix(GuestOutline* outline, const GuestOutlineFuncs* functions, void* user) { + if (!outline) return FT_Err_Invalid_Outline; + if (!functions) return FT_Err_Invalid_Argument; + const std::int32_t shift = functions->shift; + const std::int64_t delta = functions->delta; + std::int32_t previousEnd = -1; + for (std::uint32_t contour = 0; contour < outline->n_contours; ++contour) { + const std::int32_t first = previousEnd + 1; + const std::int32_t last = outline->contours[contour]; + if (last < first) return FT_Err_Invalid_Outline; + previousEnd = last; + auto* points = outline->points; + auto* tags = outline->tags; + std::int32_t limit = last; + std::int32_t point = first; + GuestVector start = Scale(points[first], shift, delta); + const GuestVector end = Scale(points[last], shift, delta); + const int firstTag = tags[first] & 3; + if (firstTag == FT_CURVE_TAG_CUBIC) return FT_Err_Invalid_Outline; + if (firstTag == FT_CURVE_TAG_CONIC) { + if ((tags[last] & 3) == FT_CURVE_TAG_ON) { + start = end; + --limit; + } else { + start = Midpoint(start, end); + } + --point; + } + int error = functions->move_to(&start, user); + if (error != 0) return error; + bool closed = false; + while (point < limit) { + ++point; + const int tag = tags[point] & 3; + if (tag == FT_CURVE_TAG_ON) { + const GuestVector to = Scale(points[point], shift, delta); + error = functions->line_to(&to, user); + if (error != 0) return error; + } else if (tag == FT_CURVE_TAG_CONIC) { + GuestVector control = Scale(points[point], shift, delta); + while (true) { + if (point == limit) { + error = functions->conic_to(&control, &start, user); + closed = true; + break; + } + ++point; + const int nextTag = tags[point] & 3; + const GuestVector next = Scale(points[point], shift, delta); + if (nextTag == FT_CURVE_TAG_ON) { + error = functions->conic_to(&control, &next, user); + break; + } + if (nextTag != FT_CURVE_TAG_CONIC) return FT_Err_Invalid_Outline; + const GuestVector middle = Midpoint(control, next); + error = functions->conic_to(&control, &middle, user); + if (error != 0) return error; + control = next; + } + if (error != 0) return error; + } else { + if (point + 1 > limit || (tags[point + 1] & 3) != FT_CURVE_TAG_CUBIC) return FT_Err_Invalid_Outline; + const GuestVector control1 = Scale(points[point], shift, delta); + const GuestVector control2 = Scale(points[point + 1], shift, delta); + point += 2; + if (point <= limit) { + const GuestVector to = Scale(points[point], shift, delta); + error = functions->cubic_to(&control1, &control2, &to, user); + } else { + error = functions->cubic_to(&control1, &control2, &start, user); + closed = true; + } + if (error != 0) return error; + } + } + if (!closed) { + error = functions->line_to(&start, user); + if (error != 0) return error; + } + } + return 0; +} diff --git a/core/libs/prx/libSceFreeType/src/Pfr.cpp b/core/libs/prx/libSceFreeType/src/Pfr.cpp new file mode 100644 index 0000000000..d6e56c56cd --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/Pfr.cpp @@ -0,0 +1,48 @@ +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libSceFreeType/include/GuestPfr.hpp" +#include FT_FONT_FORMATS_H +#include FT_PFR_H +#include + +extern "C" { + +int FreeTypePfrAdvance_nid_no_patch(FreeType::GuestFaceRec* face, std::uint32_t index, std::int64_t* advance) { + if (!face) return FT_Err_Invalid_Argument; + const auto* owner = FreeType::FaceOf(face); + const FT_Face host = owner ? owner->host : nullptr; + if (!host || !advance) return FT_Get_PFR_Advance(host, index, nullptr); + FT_Pos value = 0; + const auto error = FT_Get_PFR_Advance(host, index, &value); + const char* format = FT_Get_Font_Format(host); + if (error == 0 || (format && std::strcmp(format, "PFR") == 0)) *advance = value; + return error; +} + +int FreeTypePfrKerning_nid_no_patch(FreeType::GuestFaceRec* face, std::uint32_t left, std::uint32_t right, FreeType::GuestVector* vector) { + if (!face) return FT_Err_Invalid_Argument; + const auto* owner = FreeType::FaceOf(face); + const FT_Face host = owner ? owner->host : nullptr; + if (!host || !vector) return FT_Get_PFR_Kerning(host, left, right, nullptr); + FT_Vector value{}; + const auto error = FT_Get_PFR_Kerning(host, left, right, &value); + *vector = {value.x, value.y}; + return error; +} + +int FreeTypePfrMetrics_nid_no_patch(FreeType::GuestFaceRec* face, std::uint32_t* outline, std::uint32_t* metrics, std::int64_t* xScale, std::int64_t* yScale) { + if (!face) return FT_Err_Invalid_Argument; + const auto* owner = FreeType::FaceOf(face); + const FT_Face host = owner ? owner->host : nullptr; + if (!host) return FT_Get_PFR_Metrics(nullptr, nullptr, nullptr, nullptr, nullptr); + FT_UInt hostOutline = 0, hostMetrics = 0; + FT_Fixed hostX = 0, hostY = 0; + const auto error = FT_Get_PFR_Metrics(host, outline ? &hostOutline : nullptr, metrics ? &hostMetrics : nullptr, + xScale ? &hostX : nullptr, yScale ? &hostY : nullptr); + if (outline) *outline = hostOutline; + if (metrics) *metrics = hostMetrics; + if (xScale) *xScale = hostX; + if (yScale) *yScale = hostY; + return error; +} + +} diff --git a/core/libs/prx/libSceFreeTypeOl/CMakeLists.txt b/core/libs/prx/libSceFreeTypeOl/CMakeLists.txt new file mode 100644 index 0000000000..e8f18eac97 --- /dev/null +++ b/core/libs/prx/libSceFreeTypeOl/CMakeLists.txt @@ -0,0 +1,38 @@ +add_library(libSceFreeTypeOl SHARED EXCLUDE_FROM_ALL Export.cpp ../libSceFreeType/src/OutlineBorders.cpp) +target_include_directories(libSceFreeTypeOl PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceFreeTypeOl) + +if(BUILD_TESTING) + add_test_executable(guest_freetype_outline_border_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeOutlineBorder.cpp) + target_include_directories(guest_freetype_outline_border_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_outline_border_tests PRIVATE freetype ${CMAKE_DL_LIBS}) + if(WIN32) + target_link_libraries(guest_freetype_outline_border_tests PRIVATE libc) + endif() + configure_windows_unwind(guest_freetype_outline_border_tests) + add_custom_target(freetype_outline_border_patched DEPENDS + ${PATCHED_DIR}/libSceFreeTypeOl.prx ${PATCHED_DIR}/libSceFreeTypeOt.prx ${PATCHED_DIR}/libkernel.prx ${LIBC_PATCHED}) + add_dependencies(guest_freetype_outline_border_tests freetype_outline_border_patched) + add_test(NAME guest_freetype_outline_border_reference COMMAND guest_freetype_outline_border_tests reference) + foreach(module IN ITEMS Ol Ot) + foreach(border IN ITEMS inside outside) + add_test(NAME guest_freetype_outline_border_${module}_${border} + COMMAND guest_freetype_outline_border_tests $ ${border} raw) + add_test(NAME guest_freetype_outline_border_${module}_${border}_patched + COMMAND guest_freetype_outline_border_tests ${PATCHED_DIR}/libSceFreeType${module}.prx ${border} patched) + set_tests_properties(guest_freetype_outline_border_${module}_${border} + guest_freetype_outline_border_${module}_${border}_patched PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_freetype_outline_border_${module}_${border} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(guest_freetype_outline_border_${module}_${border}_patched PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:${PATCHED_DIR}") + endif() + endforeach() + endforeach() + set_tests_properties(guest_freetype_outline_border_reference PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_freetype_outline_border_reference PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceFreeTypeOl/libSceFreeTypeOl.hpp b/core/libs/prx/libSceFreeTypeOl/libSceFreeTypeOl.hpp index 6f70f09bee..e1970ef93b 100644 --- a/core/libs/prx/libSceFreeTypeOl/libSceFreeTypeOl.hpp +++ b/core/libs/prx/libSceFreeTypeOl/libSceFreeTypeOl.hpp @@ -1 +1,3 @@ #pragma once + +#include "prx/libSceFreeType/include/Api.hpp" diff --git a/core/libs/prx/libSceFreeTypeOptOl/CMakeLists.txt b/core/libs/prx/libSceFreeTypeOptOl/CMakeLists.txt new file mode 100644 index 0000000000..ed6ccae2b6 --- /dev/null +++ b/core/libs/prx/libSceFreeTypeOptOl/CMakeLists.txt @@ -0,0 +1,48 @@ +target_sources(libSceFreeType PRIVATE ${LIBS_INCLUDE_DIR}/prx/libSceFreeType/src/Pfr.cpp) + +add_library(libSceFreeTypeOptOl SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceFreeTypeOptOl PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceFreeTypeOptOl PRIVATE libSceFreeType) +configure_windows_unwind(libSceFreeTypeOptOl) + +if(BUILD_TESTING) + add_test_executable(guest_freetype_pfr_null_face_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypePfrNull.cpp + ${LIBS_INCLUDE_DIR}/prx/libSceFreeType/src/Pfr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) + target_include_directories(guest_freetype_pfr_null_face_tests PRIVATE + ${LIBS_INCLUDE_DIR} $) + configure_windows_unwind(guest_freetype_pfr_null_face_tests) + add_test(NAME guest_freetype_pfr_null_face COMMAND guest_freetype_pfr_null_face_tests) + set_tests_properties(guest_freetype_pfr_null_face PROPERTIES TIMEOUT 10) + add_test_executable(guest_freetype_pfr_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypePfr.cpp + ${LIBS_INCLUDE_DIR}/nid/src/NidCompute.cpp ${LIBS_INCLUDE_DIR}/nid/src/Sha1.cpp) + target_include_directories(guest_freetype_pfr_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_freetype_pfr_tests PRIVATE freetype ${CMAKE_DL_LIBS}) + if(WIN32) + target_link_libraries(guest_freetype_pfr_tests PRIVATE libc) + endif() + configure_windows_unwind(guest_freetype_pfr_tests) + add_custom_target(guest_freetype_pfr_libraries DEPENDS + ${PATCHED_DIR}/libSceFreeType.prx ${PATCHED_DIR}/libSceFreeTypeOptOl.prx ${PATCHED_DIR}/libkernel.prx ${LIBC_PATCHED}) + add_dependencies(guest_freetype_pfr_tests guest_freetype_pfr_libraries) + add_test(NAME guest_freetype_pfr_reference COMMAND guest_freetype_pfr_tests reference) + set_tests_properties(guest_freetype_pfr_reference PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_freetype_pfr_reference PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:${UNPATCHED_DIR}") + endif() + foreach(operation IN ITEMS metrics advance kerning) + foreach(mode IN ITEMS raw patched) + if(mode STREQUAL "raw") + set(pfrDirectory ${UNPATCHED_DIR}) + else() + set(pfrDirectory ${PATCHED_DIR}) + endif() + add_test(NAME guest_freetype_pfr_${operation}_${mode} COMMAND guest_freetype_pfr_tests ${pfrDirectory} ${mode} ${operation}) + set_tests_properties(guest_freetype_pfr_${operation}_${mode} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_freetype_pfr_${operation}_${mode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:${pfrDirectory}") + endif() + endforeach() + endforeach() +endif() diff --git a/core/libs/prx/libSceFreeTypeOptOl/Export.cpp b/core/libs/prx/libSceFreeTypeOptOl/Export.cpp index d81f4a5e32..3670e92e25 100644 --- a/core/libs/prx/libSceFreeTypeOptOl/Export.cpp +++ b/core/libs/prx/libSceFreeTypeOptOl/Export.cpp @@ -1,10 +1,18 @@ -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" +#include "prx/libSceFreeType/include/Api.hpp" +#include "prx/libSceFreeType/include/GuestPfr.hpp" extern "C" { -APS5_DUMMY_FUN +int APS5_VABI FT_Get_PFR_Advance_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t index, std::int64_t* advance) { + return FreeTypePfrAdvance_nid_no_patch(face, index, advance); +} + +int APS5_VABI FT_Get_PFR_Kerning_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t left, std::uint32_t right, FreeType::GuestVector* vector) { + return FreeTypePfrKerning_nid_no_patch(face, left, right, vector); +} + +int APS5_VABI FT_Get_PFR_Metrics_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t* outline, std::uint32_t* metrics, std::int64_t* xScale, std::int64_t* yScale) { + return FreeTypePfrMetrics_nid_no_patch(face, outline, metrics, xScale, yScale); +} } diff --git a/core/libs/prx/libSceFreeTypeOptOl/libSceFreeTypeOptOl.hpp b/core/libs/prx/libSceFreeTypeOptOl/libSceFreeTypeOptOl.hpp index 6f70f09bee..736d61af2e 100644 --- a/core/libs/prx/libSceFreeTypeOptOl/libSceFreeTypeOptOl.hpp +++ b/core/libs/prx/libSceFreeTypeOptOl/libSceFreeTypeOptOl.hpp @@ -1 +1,2 @@ #pragma once +#include "prx/libSceFreeType/include/Api.hpp" diff --git a/core/libs/prx/libSceFreeTypeOt/CMakeLists.txt b/core/libs/prx/libSceFreeTypeOt/CMakeLists.txt new file mode 100644 index 0000000000..aca3d5c0df --- /dev/null +++ b/core/libs/prx/libSceFreeTypeOt/CMakeLists.txt @@ -0,0 +1,3 @@ +add_library(libSceFreeTypeOt SHARED EXCLUDE_FROM_ALL Export.cpp ../libSceFreeType/src/OutlineBorders.cpp) +target_include_directories(libSceFreeTypeOt PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceFreeTypeOt) diff --git a/core/libs/prx/libSceFreeTypeOt/Export.cpp b/core/libs/prx/libSceFreeTypeOt/Export.cpp index d81f4a5e32..c4ec1cd9ca 100644 --- a/core/libs/prx/libSceFreeTypeOt/Export.cpp +++ b/core/libs/prx/libSceFreeTypeOt/Export.cpp @@ -5,6 +5,745 @@ extern "C" { -APS5_DUMMY_FUN + +int APS5_VABI sceFreeTypeOtUnknown_UD7IFu7ML2U_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("UD7IFu7ML2U", sceFreeTypeOtUnknown_UD7IFu7ML2U_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_2T7lbS_CSzA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("2T7lbS+CSzA", sceFreeTypeOtUnknown_2T7lbS_CSzA_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_MGEIQsujKaY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("MGEIQsujKaY", sceFreeTypeOtUnknown_MGEIQsujKaY_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_mkmgcRnAJog_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("mkmgcRnAJog", sceFreeTypeOtUnknown_mkmgcRnAJog_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_m18bTUNF_0g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("m18bTUNF-0g", sceFreeTypeOtUnknown_m18bTUNF_0g_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_vTZ34rw1sfg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("vTZ34rw1sfg", sceFreeTypeOtUnknown_vTZ34rw1sfg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_PvCNusrp9xQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("PvCNusrp9xQ", sceFreeTypeOtUnknown_PvCNusrp9xQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_dlM4ASZdcjk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("dlM4ASZdcjk", sceFreeTypeOtUnknown_dlM4ASZdcjk_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Ev3BqJ_U1Fg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Ev3BqJ-U1Fg", sceFreeTypeOtUnknown_Ev3BqJ_U1Fg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_ZhtAXOoLNyo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ZhtAXOoLNyo", sceFreeTypeOtUnknown_ZhtAXOoLNyo_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_73vg_nRH68g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("73vg+nRH68g", sceFreeTypeOtUnknown_73vg_nRH68g_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_wBKAYO7yEXw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("wBKAYO7yEXw", sceFreeTypeOtUnknown_wBKAYO7yEXw_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_nSIux4OQyJM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("nSIux4OQyJM", sceFreeTypeOtUnknown_nSIux4OQyJM_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Dr3F3qrSQ2c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Dr3F3qrSQ2c", sceFreeTypeOtUnknown_Dr3F3qrSQ2c_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_avLzN0tTxLI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("avLzN0tTxLI", sceFreeTypeOtUnknown_avLzN0tTxLI_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_0rKmI1ZbMAc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0rKmI1ZbMAc", sceFreeTypeOtUnknown_0rKmI1ZbMAc_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_BBRhYfYBDgU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("BBRhYfYBDgU", sceFreeTypeOtUnknown_BBRhYfYBDgU_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_c1nbr8oSWlg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("c1nbr8oSWlg", sceFreeTypeOtUnknown_c1nbr8oSWlg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_AVEEb57HhUw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("AVEEb57HhUw", sceFreeTypeOtUnknown_AVEEb57HhUw_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_x8y0qzLiYOU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("x8y0qzLiYOU", sceFreeTypeOtUnknown_x8y0qzLiYOU_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_VW4GD_TaIUc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("VW4GD-TaIUc", sceFreeTypeOtUnknown_VW4GD_TaIUc_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_GiuNQjoj4fs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("GiuNQjoj4fs", sceFreeTypeOtUnknown_GiuNQjoj4fs_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_5qqbXOGlHw4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("5qqbXOGlHw4", sceFreeTypeOtUnknown_5qqbXOGlHw4_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_4LY5pyZ5IQk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4LY5pyZ5IQk", sceFreeTypeOtUnknown_4LY5pyZ5IQk_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_zE0V_0aouZ4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("zE0V+0aouZ4", sceFreeTypeOtUnknown_zE0V_0aouZ4_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_LsyswTUNTRs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("LsyswTUNTRs", sceFreeTypeOtUnknown_LsyswTUNTRs_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_AfL_nqNC5fY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("AfL-nqNC5fY", sceFreeTypeOtUnknown_AfL_nqNC5fY_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_WIvTHYs7ZQQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("WIvTHYs7ZQQ", sceFreeTypeOtUnknown_WIvTHYs7ZQQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_yrtsLmTfa48_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("yrtsLmTfa48", sceFreeTypeOtUnknown_yrtsLmTfa48_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_tCMhTh7h8D4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("tCMhTh7h8D4", sceFreeTypeOtUnknown_tCMhTh7h8D4_nid_no_patch); + +int APS5_VABI sceFreeTypeOtNewSize_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("zyezaHweY3U", sceFreeTypeOtNewSize_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_C_PpdS5zris_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("C-PpdS5zris", sceFreeTypeOtUnknown_C_PpdS5zris_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_g8eLxL9VAhU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("g8eLxL9VAhU", sceFreeTypeOtUnknown_g8eLxL9VAhU_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_OeNH41MA2Vs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("OeNH41MA2Vs", sceFreeTypeOtUnknown_OeNH41MA2Vs_nid_no_patch); + +int APS5_VABI sceFreeTypeOtOutlineCopy_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Bhm8Injl0KA", sceFreeTypeOtOutlineCopy_nid_no_patch); + +int APS5_VABI sceFreeTypeOtOutlineDecompose_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("++lgV9pRsO4", sceFreeTypeOtOutlineDecompose_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_BqGJsJcFYaM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("BqGJsJcFYaM", sceFreeTypeOtUnknown_BqGJsJcFYaM_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_umIqupy8UU0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("umIqupy8UU0", sceFreeTypeOtUnknown_umIqupy8UU0_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_l6Cf9rb9fZQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("l6Cf9rb9fZQ", sceFreeTypeOtUnknown_l6Cf9rb9fZQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_BgUMBGvhATU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("BgUMBGvhATU", sceFreeTypeOtUnknown_BgUMBGvhATU_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_0Aal_TIC4so_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0Aal-TIC4so", sceFreeTypeOtUnknown_0Aal_TIC4so_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_4RRjK_Wd1lU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4RRjK-Wd1lU", sceFreeTypeOtUnknown_4RRjK_Wd1lU_nid_no_patch); + +int APS5_VABI sceFreeTypeOtOutlineGetOrientation_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("weVsBCT+TTQ", sceFreeTypeOtOutlineGetOrientation_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_QH2emcgeFck_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("QH2emcgeFck", sceFreeTypeOtUnknown_QH2emcgeFck_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_NG7OgYsgcF8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("NG7OgYsgcF8", sceFreeTypeOtUnknown_NG7OgYsgcF8_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_9km02mPqPJE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("9km02mPqPJE", sceFreeTypeOtUnknown_9km02mPqPJE_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_0Lv6K1d47jg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0Lv6K1d47jg", sceFreeTypeOtUnknown_0Lv6K1d47jg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_8fAMdZCnewA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("8fAMdZCnewA", sceFreeTypeOtUnknown_8fAMdZCnewA_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_LsB538_IPrE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("LsB538+IPrE", sceFreeTypeOtUnknown_LsB538_IPrE_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_hz_J3hohXW8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("hz-J3hohXW8", sceFreeTypeOtUnknown_hz_J3hohXW8_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_KVbmKCGGowg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("KVbmKCGGowg", sceFreeTypeOtUnknown_KVbmKCGGowg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_hF3stKUWbWg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("hF3stKUWbWg", sceFreeTypeOtUnknown_hF3stKUWbWg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_p5GNJaXO2yE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("p5GNJaXO2yE", sceFreeTypeOtUnknown_p5GNJaXO2yE_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_8cnVx1yH9_I_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("8cnVx1yH9-I", sceFreeTypeOtUnknown_8cnVx1yH9_I_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_HVSaZbxYi2s_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("HVSaZbxYi2s", sceFreeTypeOtUnknown_HVSaZbxYi2s_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_qAPXM5DN62Q_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("qAPXM5DN62Q", sceFreeTypeOtUnknown_qAPXM5DN62Q_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_5ipBcLYVc2k_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("5ipBcLYVc2k", sceFreeTypeOtUnknown_5ipBcLYVc2k_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_8A0cAU4yBVY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("8A0cAU4yBVY", sceFreeTypeOtUnknown_8A0cAU4yBVY_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_z9Wsaf34JA4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("z9Wsaf34JA4", sceFreeTypeOtUnknown_z9Wsaf34JA4_nid_no_patch); + +int APS5_VABI sceFreeTypeOtRequestSize_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("PaDk9bdmdhU", sceFreeTypeOtRequestSize_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Z8KG6700sw4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Z8KG6700sw4", sceFreeTypeOtUnknown_Z8KG6700sw4_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_uK7vGxAQVxM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("uK7vGxAQVxM", sceFreeTypeOtUnknown_uK7vGxAQVxM_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_zetC8Tp1S8w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("zetC8Tp1S8w", sceFreeTypeOtUnknown_zetC8Tp1S8w_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_DAmwG6U7YIc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("DAmwG6U7YIc", sceFreeTypeOtUnknown_DAmwG6U7YIc_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Yu9oXRVmTTo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Yu9oXRVmTTo", sceFreeTypeOtUnknown_Yu9oXRVmTTo_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_ihIHD8BTNxI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ihIHD8BTNxI", sceFreeTypeOtUnknown_ihIHD8BTNxI_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_yCEJIsjIOO8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("yCEJIsjIOO8", sceFreeTypeOtUnknown_yCEJIsjIOO8_nid_no_patch); + +int APS5_VABI sceFreeTypeOtSin_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("L1uHkWTazkI", sceFreeTypeOtSin_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_C7N2GuwqTA0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("C7N2GuwqTA0", sceFreeTypeOtUnknown_C7N2GuwqTA0_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_JgtSc0g3G6E_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("JgtSc0g3G6E", sceFreeTypeOtUnknown_JgtSc0g3G6E_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_gVf7xRLdWm8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("gVf7xRLdWm8", sceFreeTypeOtUnknown_gVf7xRLdWm8_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_FHdeCFVf_f8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("FHdeCFVf-f8", sceFreeTypeOtUnknown_FHdeCFVf_f8_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Qmgc0XACzRg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Qmgc0XACzRg", sceFreeTypeOtUnknown_Qmgc0XACzRg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_0rGx_oFMLyk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0rGx-oFMLyk", sceFreeTypeOtUnknown_0rGx_oFMLyk_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_thA48N_0VwQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("thA48N-0VwQ", sceFreeTypeOtUnknown_thA48N_0VwQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_jl0VxYZb8_o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("jl0VxYZb8-o", sceFreeTypeOtUnknown_jl0VxYZb8_o_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Er0Lin_u20o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Er0Lin-u20o", sceFreeTypeOtUnknown_Er0Lin_u20o_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_KHj92a3WDn4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("KHj92a3WDn4", sceFreeTypeOtUnknown_KHj92a3WDn4_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_tDssQCH2syY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("tDssQCH2syY", sceFreeTypeOtUnknown_tDssQCH2syY_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_dXJK59vXRxs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("dXJK59vXRxs", sceFreeTypeOtUnknown_dXJK59vXRxs_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_BB4gl7SgLdk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("BB4gl7SgLdk", sceFreeTypeOtUnknown_BB4gl7SgLdk_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Fv8MWCUH8k8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Fv8MWCUH8k8", sceFreeTypeOtUnknown_Fv8MWCUH8k8_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_RzgDJTjQst4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("RzgDJTjQst4", sceFreeTypeOtUnknown_RzgDJTjQst4_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_4yt9cD1qcEI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4yt9cD1qcEI", sceFreeTypeOtUnknown_4yt9cD1qcEI_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_u6Xquj1bFwc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("u6Xquj1bFwc", sceFreeTypeOtUnknown_u6Xquj1bFwc_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown__Xm1cLEpBec_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-Xm1cLEpBec", sceFreeTypeOtUnknown__Xm1cLEpBec_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_uDXTnGbAz44_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("uDXTnGbAz44", sceFreeTypeOtUnknown_uDXTnGbAz44_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_iruPsrXCN7U_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("iruPsrXCN7U", sceFreeTypeOtUnknown_iruPsrXCN7U_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_a7RlARfexoo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("a7RlARfexoo", sceFreeTypeOtUnknown_a7RlARfexoo_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_jX1zq1i8KfI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("jX1zq1i8KfI", sceFreeTypeOtUnknown_jX1zq1i8KfI_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_U_0WKVAePww_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("U+0WKVAePww", sceFreeTypeOtUnknown_U_0WKVAePww_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_dlbnv6Wass8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("dlbnv6Wass8", sceFreeTypeOtUnknown_dlbnv6Wass8_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_cmd41395g7Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("cmd41395g7Y", sceFreeTypeOtUnknown_cmd41395g7Y_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_SX5I_XzH8HI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("SX5I+XzH8HI", sceFreeTypeOtUnknown_SX5I_XzH8HI_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Bmi8PdvsM3M_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Bmi8PdvsM3M", sceFreeTypeOtUnknown_Bmi8PdvsM3M_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_jJh4SMt8NFQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("jJh4SMt8NFQ", sceFreeTypeOtUnknown_jJh4SMt8NFQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_1z6bBg1qBfU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("1z6bBg1qBfU", sceFreeTypeOtUnknown_1z6bBg1qBfU_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_mF0B64O954g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("mF0B64O954g", sceFreeTypeOtUnknown_mF0B64O954g_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_c002A4zTOgA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("c002A4zTOgA", sceFreeTypeOtUnknown_c002A4zTOgA_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_mcT796Qgj_g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("mcT796Qgj+g", sceFreeTypeOtUnknown_mcT796Qgj_g_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_b5gZp_r3WWI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("b5gZp-r3WWI", sceFreeTypeOtUnknown_b5gZp_r3WWI_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_S_n0Md8Rjfw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("S+n0Md8Rjfw", sceFreeTypeOtUnknown_S_n0Md8Rjfw_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_6a1p3QgXvo0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("6a1p3QgXvo0", sceFreeTypeOtUnknown_6a1p3QgXvo0_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_JFSCOXTEvfg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("JFSCOXTEvfg", sceFreeTypeOtUnknown_JFSCOXTEvfg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_xejRyEbsLcw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("xejRyEbsLcw", sceFreeTypeOtUnknown_xejRyEbsLcw_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_R1mZutnlTiM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("R1mZutnlTiM", sceFreeTypeOtUnknown_R1mZutnlTiM_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_4J4Baf8ana8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4J4Baf8ana8", sceFreeTypeOtUnknown_4J4Baf8ana8_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Az4H9FlfxCA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Az4H9FlfxCA", sceFreeTypeOtUnknown_Az4H9FlfxCA_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_xj3tNJptfLI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("xj3tNJptfLI", sceFreeTypeOtUnknown_xj3tNJptfLI_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_TPKhRt1QIeE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("TPKhRt1QIeE", sceFreeTypeOtUnknown_TPKhRt1QIeE_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_6_yXRlVrjbc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("6+yXRlVrjbc", sceFreeTypeOtUnknown_6_yXRlVrjbc_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown__RLa_6fbshA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+RLa-6fbshA", sceFreeTypeOtUnknown__RLa_6fbshA_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_pBqh_Uh_gns_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("pBqh-Uh-gns", sceFreeTypeOtUnknown_pBqh_Uh_gns_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_2rcvKZfm5rI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("2rcvKZfm5rI", sceFreeTypeOtUnknown_2rcvKZfm5rI_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_7cIS2cFG3PE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("7cIS2cFG3PE", sceFreeTypeOtUnknown_7cIS2cFG3PE_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_LfyUi_LkdCk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("LfyUi-LkdCk", sceFreeTypeOtUnknown_LfyUi_LkdCk_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_rzIWh7FJnmU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("rzIWh7FJnmU", sceFreeTypeOtUnknown_rzIWh7FJnmU_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_cKndiZnt0EA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("cKndiZnt0EA", sceFreeTypeOtUnknown_cKndiZnt0EA_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_eqGCaeizvWM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("eqGCaeizvWM", sceFreeTypeOtUnknown_eqGCaeizvWM_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_RvFx7dv9ThU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("RvFx7dv9ThU", sceFreeTypeOtUnknown_RvFx7dv9ThU_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Po8kyd3DlIw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Po8kyd3DlIw", sceFreeTypeOtUnknown_Po8kyd3DlIw_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_B3uU5aDZn94_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("B3uU5aDZn94", sceFreeTypeOtUnknown_B3uU5aDZn94_nid_no_patch); + +int APS5_VABI sceFreeTypeOtTan_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("dojyaLKzKN0", sceFreeTypeOtTan_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_08_mhD_AAds_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("08-mhD-AAds", sceFreeTypeOtUnknown_08_mhD_AAds_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_64XMq6sDxUo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("64XMq6sDxUo", sceFreeTypeOtUnknown_64XMq6sDxUo_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_4xG87W9dTSg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4xG87W9dTSg", sceFreeTypeOtUnknown_4xG87W9dTSg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_4_SScYDT5Pg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4+SScYDT5Pg", sceFreeTypeOtUnknown_4_SScYDT5Pg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_2rmvbvOCyjY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("2rmvbvOCyjY", sceFreeTypeOtUnknown_2rmvbvOCyjY_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_1jmC2QcpRl0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("1jmC2QcpRl0", sceFreeTypeOtUnknown_1jmC2QcpRl0_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_HMcNe70D9TI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("HMcNe70D9TI", sceFreeTypeOtUnknown_HMcNe70D9TI_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_6wJwUIVwAxg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("6wJwUIVwAxg", sceFreeTypeOtUnknown_6wJwUIVwAxg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_PrZtZyFwM6c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("PrZtZyFwM6c", sceFreeTypeOtUnknown_PrZtZyFwM6c_nid_no_patch); + +int APS5_VABI sceFreeTypeOtVectorUnit_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("3WpTQrvGUcU", sceFreeTypeOtVectorUnit_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_P6WOAeb0lJ8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("P6WOAeb0lJ8", sceFreeTypeOtUnknown_P6WOAeb0lJ8_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_XGVABnpNksQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("XGVABnpNksQ", sceFreeTypeOtUnknown_XGVABnpNksQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Ln9I6YfmYJU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Ln9I6YfmYJU", sceFreeTypeOtUnknown_Ln9I6YfmYJU_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_7cVMwHmN308_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("7cVMwHmN308", sceFreeTypeOtUnknown_7cVMwHmN308_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_AEU2tkjdZ2w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("AEU2tkjdZ2w", sceFreeTypeOtUnknown_AEU2tkjdZ2w_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_2lSRg9FfIOY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("2lSRg9FfIOY", sceFreeTypeOtUnknown_2lSRg9FfIOY_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_fPrLu1YGOaw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("fPrLu1YGOaw", sceFreeTypeOtUnknown_fPrLu1YGOaw_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_t3vCgnT5YL0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("t3vCgnT5YL0", sceFreeTypeOtUnknown_t3vCgnT5YL0_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_8bkQ_YRnpOE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("8bkQ+YRnpOE", sceFreeTypeOtUnknown_8bkQ_YRnpOE_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_q4qhGebYuGI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("q4qhGebYuGI", sceFreeTypeOtUnknown_q4qhGebYuGI_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_ni_K_leoQNg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ni-K-leoQNg", sceFreeTypeOtUnknown_ni_K_leoQNg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_XfcgJB6V2a0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("XfcgJB6V2a0", sceFreeTypeOtUnknown_XfcgJB6V2a0_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_TWzf5iykiGw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("TWzf5iykiGw", sceFreeTypeOtUnknown_TWzf5iykiGw_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown__b4iVKArt94_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-b4iVKArt94", sceFreeTypeOtUnknown__b4iVKArt94_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_uSNqxrARpEQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("uSNqxrARpEQ", sceFreeTypeOtUnknown_uSNqxrARpEQ_nid_no_patch); } diff --git a/core/libs/prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp b/core/libs/prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp index 6f70f09bee..e1970ef93b 100644 --- a/core/libs/prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp +++ b/core/libs/prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp @@ -1 +1,3 @@ #pragma once + +#include "prx/libSceFreeType/include/Api.hpp" diff --git a/core/libs/prx/libSceFsInternalForVsh/Export.cpp b/core/libs/prx/libSceFsInternalForVsh/Export.cpp index 75159006f0..e98aa4a828 100644 --- a/core/libs/prx/libSceFsInternalForVsh/Export.cpp +++ b/core/libs/prx/libSceFsInternalForVsh/Export.cpp @@ -212,13 +212,11 @@ int APS5_VABI sceFsDrfsResumeSocket() { } int APS5_VABI sceFsErriCtl() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x8002002d); } int APS5_VABI sceFsErriGetState() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x8002002d); } int APS5_VABI sceFsExfatMkfsExternalHdd() { diff --git a/core/libs/prx/libSceGameUpdate/Export.cpp b/core/libs/prx/libSceGameUpdate/Export.cpp index dfcb3d1383..0e3ef3835e 100644 --- a/core/libs/prx/libSceGameUpdate/Export.cpp +++ b/core/libs/prx/libSceGameUpdate/Export.cpp @@ -1,7 +1,9 @@ #include "prx/libSceGameUpdate/include/Api.hpp" +#include #include #include #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -11,20 +13,58 @@ static constexpr int SCE_GAME_UPDATE_ERROR_ALREADY_INITIALIZED = static_cast(0x80412803); static constexpr int SCE_GAME_UPDATE_ERROR_INVALID_SIZE = static_cast(0x80412804); static constexpr int SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND = static_cast(0x80412805); +static constexpr int SCE_GAME_UPDATE_ERROR_TOO_MANY_REQUESTS = static_cast(0x80412806); +static constexpr int SCE_GAME_UPDATE_ERROR_ABORTED = static_cast(0x80412807); +static constexpr int FIRST_REQUEST_ID = 0x20000000; +static constexpr int LAST_REQUEST_ID = 0x2FFFFFFF; static_assert(sizeof(GameUpdateCheckParam) == 0x30); static_assert(sizeof(GameUpdateCheckResult) == 0x30); -static std::atomic initialized{false}; +struct Request { + int id; + bool aborted; +}; -static int checkRequestId(int requestId) { +static std::mutex stateMutex; +static bool initialized = false; +static int nextRequestId = FIRST_REQUEST_ID; +static std::array requests{}; + +static Request* findRequest(int requestId) { + for (Request& request : requests) { + if (request.id != 0 && request.id == requestId) { + return &request; + } + } + return nullptr; +} + +static int followingRequestId(int requestId) { + return requestId == LAST_REQUEST_ID ? FIRST_REQUEST_ID : requestId + 1; +} + +static int takeRequestId() { + int requestId = nextRequestId; + while (findRequest(requestId) != nullptr) { + requestId = followingRequestId(requestId); + } + nextRequestId = followingRequestId(requestId); + return requestId; +} + +static int checkRequestId(int requestId, Request** request) { if (requestId <= 0) { return SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND; } - if (!initialized.load()) { + if (!initialized) { return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; } - return SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND; + *request = findRequest(requestId); + if (*request == nullptr) { + return SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND; + } + return 0; } static bool hasNonZeroReserved(const GameUpdateCheckParam* param) { @@ -39,7 +79,13 @@ static bool hasNonZeroReserved(const GameUpdateCheckParam* param) { extern "C" { int APS5_VABI sceGameUpdateAbortRequest(int requestId) { - return checkRequestId(requestId); + std::lock_guard lock(stateMutex); + Request* request = nullptr; + if (int error = checkRequestId(requestId, &request); error != 0) { + return error; + } + request->aborted = true; + return 0; } int APS5_VABI sceGameUpdateCheck(int requestId, const GameUpdateCheckParam* param, GameUpdateCheckResult* result) { @@ -55,18 +101,43 @@ int APS5_VABI sceGameUpdateCheck(int requestId, const GameUpdateCheckParam* para if (param->option != 0 || hasNonZeroReserved(param)) { return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; } - return checkRequestId(requestId); + std::lock_guard lock(stateMutex); + Request* request = nullptr; + if (int error = checkRequestId(requestId, &request); error != 0) { + return error; + } + if (request->aborted) { + return SCE_GAME_UPDATE_ERROR_ABORTED; + } + result->found = false; + result->addcont_found = false; + return 0; } int APS5_VABI sceGameUpdateCreateRequest(void) { - if (!initialized.load()) { + std::lock_guard lock(stateMutex); + if (!initialized) { return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; } - throw std::runtime_error("sceGameUpdateCreateRequest: patch service request creation is not implemented"); + for (Request& request : requests) { + if (request.id != 0) { + continue; + } + const int requestId = takeRequestId(); + request = {requestId, false}; + return requestId; + } + return SCE_GAME_UPDATE_ERROR_TOO_MANY_REQUESTS; } int APS5_VABI sceGameUpdateDeleteRequest(int requestId) { - return checkRequestId(requestId); + std::lock_guard lock(stateMutex); + Request* request = nullptr; + if (int error = checkRequestId(requestId, &request); error != 0) { + return error; + } + *request = {}; + return 0; } int APS5_VABI sceGameUpdateGetAddcontLatestVersion(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, GameUpdateAddcontVersionInfo* info) { @@ -77,23 +148,31 @@ int APS5_VABI sceGameUpdateGetAddcontLatestVersion(uint32_t serviceLabel, const if (entitlementLabel == nullptr) { return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; } - if (!initialized.load()) { - return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; + { + std::lock_guard lock(stateMutex); + if (!initialized) { + return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; + } } throw std::runtime_error("sceGameUpdateGetAddcontLatestVersion: entitlement version lookup is not implemented"); } int APS5_VABI sceGameUpdateInitialize(void) { - if (initialized.exchange(true)) { + std::lock_guard lock(stateMutex); + if (initialized) { return SCE_GAME_UPDATE_ERROR_ALREADY_INITIALIZED; } + initialized = true; return 0; } int APS5_VABI sceGameUpdateTerminate(void) { - if (!initialized.exchange(false)) { + std::lock_guard lock(stateMutex); + if (!initialized) { return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; } + initialized = false; + requests = {}; return 0; } diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index a96c767d8b..82c55782b7 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -382,10 +382,6 @@ int APS5_VABI sceHmd2SetIpdOffset(std::uint8_t enabled, float offset) { } APS5_EXPORT("-kZT7X7OMg0", Hmd2BeforeInitializeZero); APS5_EXPORT("1ewLtng-SVk", Hmd2BeforeInitializeZero); -APS5_EXPORT("B0b8DeIUGes", Hmd2BeforeInitializeZero); -APS5_EXPORT("HclLg4Oj5yw", Hmd2BeforeInitializeZero); -APS5_EXPORT("VbEvrQXsZyY", Hmd2BeforeInitializeZero); -APS5_EXPORT("bB6MKZi5UCQ", Hmd2BeforeInitializeZero); APS5_EXPORT("OXQ1EDf02XU", Hmd2BeforeInitializeZero); APS5_EXPORT("IAxfMTvFm0M", Hmd2BeforeInitializeZero); APS5_EXPORT("qTJtAtaLeno", Hmd2BeforeInitializeZero); @@ -542,10 +538,6 @@ std::int32_t APS5_VABI Hmd2BeforeInitializeMinusOne() { } APS5_EXPORT("EAkv8Mqt33E", Hmd2BeforeInitializeMinusOne); APS5_EXPORT("Rv7bkmKvmz0", Hmd2BeforeInitializeMinusOne); -APS5_EXPORT("1oPZizn0SNA", Hmd2BeforeInitializeMinusOne); -APS5_EXPORT("TwCpE-nNrlo", Hmd2BeforeInitializeMinusOne); -APS5_EXPORT("jkqAeQfdtxw", Hmd2BeforeInitializeMinusOne); -APS5_EXPORT("+l0RQph81Z8", Hmd2BeforeInitializeMinusOne); APS5_EXPORT("qsmAtGw1Ncc", Hmd2BeforeInitializeMinusOne); APS5_EXPORT("uKhwYoMsIsA", Hmd2BeforeInitializeMinusOne); APS5_EXPORT("WNdaigr94dI", Hmd2BeforeInitializeMinusOne); @@ -553,6 +545,38 @@ APS5_EXPORT("P6S5ZVWf-H0", Hmd2BeforeInitializeMinusOne); APS5_EXPORT("sun6NWE1sdg", Hmd2BeforeInitializeMinusOne); APS5_EXPORT("--TFsJYXo04", Hmd2BeforeInitializeMinusOne); +std::int32_t APS5_VABI sceHmd2InternalTrackingOpen() { + return BeforeInitialize(0, __func__); +} + +std::int32_t APS5_VABI sceHmd2InternalTrackingClose() { + return BeforeInitialize(0, __func__); +} + +std::int32_t APS5_VABI sceHmd2InternalLedDetectorOpen() { + return BeforeInitialize(0, __func__); +} + +std::int32_t APS5_VABI sceHmd2InternalLedDetectorClose() { + return BeforeInitialize(0, __func__); +} + +std::int32_t APS5_VABI sceHmd2InternalTrackingStart() { + return BeforeInitialize(-1, __func__); +} + +std::int32_t APS5_VABI sceHmd2InternalTrackingStop() { + return BeforeInitialize(-1, __func__); +} + +std::int32_t APS5_VABI sceHmd2InternalLedDetectorStart() { + return BeforeInitialize(-1, __func__); +} + +std::int32_t APS5_VABI sceHmd2InternalLedDetectorStop() { + return BeforeInitialize(-1, __func__); +} + std::int32_t APS5_VABI Hmd2BeforeInitializeValue0400() { return BeforeInitialize(0x400, __func__); } diff --git a/core/libs/prx/libSceHmd2/include/Api.hpp b/core/libs/prx/libSceHmd2/include/Api.hpp index 918b5ceac7..141cd26128 100644 --- a/core/libs/prx/libSceHmd2/include/Api.hpp +++ b/core/libs/prx/libSceHmd2/include/Api.hpp @@ -314,6 +314,14 @@ std::int32_t APS5_VABI Hmd2BeforeInitializeError1003(); std::int32_t APS5_VABI Hmd2BeforeInitializeMinusOne(); std::int32_t APS5_VABI Hmd2BeforeInitializeValue0400(); std::int32_t APS5_VABI Hmd2BeforeInitializeValue20000(); +std::int32_t APS5_VABI sceHmd2InternalTrackingOpen(); +std::int32_t APS5_VABI sceHmd2InternalTrackingStart(); +std::int32_t APS5_VABI sceHmd2InternalTrackingStop(); +std::int32_t APS5_VABI sceHmd2InternalTrackingClose(); +std::int32_t APS5_VABI sceHmd2InternalLedDetectorOpen(); +std::int32_t APS5_VABI sceHmd2InternalLedDetectorStart(); +std::int32_t APS5_VABI sceHmd2InternalLedDetectorStop(); +std::int32_t APS5_VABI sceHmd2InternalLedDetectorClose(); int APS5_VABI sceHmd2InternalSetPortStatus(void); int APS5_VABI sceHmd2InternalLedDetectorStartBySlot(void); int APS5_VABI sceHmd2InternalImageStartBySlot(void); diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index ad94bc3ba5..b9a1bfa606 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -111,6 +111,7 @@ struct HttpEpoll { static std::mutex g_stateMutex; static std::unordered_set g_contexts; static std::unordered_map g_contextPoolSizes; +static std::unordered_map> g_contextRegisteredIds; static std::unordered_map> g_epolls; static std::shared_ptr FindEpoll(HttpEpollHandle eh) { @@ -276,11 +277,10 @@ int APS5_VABI sceHttpGetStatusCode(int request_id, int* status_code) { } int APS5_VABI sceHttpInit_nid_postfix(int memid, int ssl_ctx_id, uint64_t pool_size) { - (void)memid; - (void)ssl_ctx_id; const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); std::lock_guard lock(g_stateMutex); g_contextPoolSizes[id] = pool_size; + g_contextRegisteredIds[id] = {memid, ssl_ctx_id}; g_contexts.insert(id); AddHttpContext(id); return id; @@ -404,6 +404,7 @@ int APS5_VABI sceHttpTerm_nid_postfix(int http_ctx_id) { { std::lock_guard lock(g_stateMutex); g_contextPoolSizes.erase(http_ctx_id); + g_contextRegisteredIds.erase(http_ctx_id); if (g_contexts.empty()) return ERROR_BEFORE_INIT; if (g_contexts.erase(http_ctx_id) == 0) return ERROR_INVALID_ID; RemoveHttpContext(http_ctx_id); @@ -824,7 +825,13 @@ int APS5_VABI sceHttpSetHttp09Enabled(int id, int enabled) { } int APS5_VABI sceHttpGetRegisteredCtxIds(int contextId, int* netPoolId, int* sslContextId) { - NotImplemented_nid_no_patch(__func__); + if (!netPoolId || !sslContextId) APS5_INVALID_ARG_EX; + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + const auto ids = g_contextRegisteredIds.find(contextId); + if (ids == g_contextRegisteredIds.end()) return ERROR_INVALID_ID; + *netPoolId = ids->second.first; + *sslContextId = ids->second.second; return 0; } diff --git a/core/libs/prx/libSceHttp2/CMakeLists.txt b/core/libs/prx/libSceHttp2/CMakeLists.txt index 7fbaa4014f..5ab00a0e4f 100644 --- a/core/libs/prx/libSceHttp2/CMakeLists.txt +++ b/core/libs/prx/libSceHttp2/CMakeLists.txt @@ -1,6 +1,33 @@ +function(configure_http2_backend) + set(ENABLE_LIB_ONLY ON) + set(ENABLE_DOC OFF) + set(BUILD_SHARED_LIBS OFF) + set(BUILD_STATIC_LIBS ON) + foreach(package OpenSSL Libngtcp2 Libnghttp3 Systemd Jansson Libevent LibXml2 Jemalloc) + set(CMAKE_DISABLE_FIND_PACKAGE_${package} TRUE) + endforeach() + add_subdirectory(${CMAKE_SOURCE_DIR}/3rdparty/nghttp2 ${CMAKE_BINARY_DIR}/3rdparty/nghttp2 EXCLUDE_FROM_ALL) +endfunction() +configure_http2_backend() +set_target_properties(nghttp2_static PROPERTIES POSITION_INDEPENDENT_CODE ON C_VISIBILITY_PRESET hidden) +if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(nghttp2_static INTERFACE LINKER:--exclude-libs,libnghttp2.a) +endif() +install(FILES ${CMAKE_SOURCE_DIR}/3rdparty/nghttp2/COPYING DESTINATION licenses/nghttp2) + +add_library(anyps5_http2_transport STATIC EXCLUDE_FROM_ALL Transport.cpp) +target_include_directories(anyps5_http2_transport PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(anyps5_http2_transport PRIVATE libSceNet libSceSsl libkernel libc nghttp2_static) +set_target_properties(anyps5_http2_transport PROPERTIES POSITION_INDEPENDENT_CODE ON CXX_VISIBILITY_PRESET hidden) +if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(anyps5_http2_transport INTERFACE LINKER:--exclude-libs,libanyps5_http2_transport.a) +endif() add_library(libSceHttp2 SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceHttp2 PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(libSceHttp2 PRIVATE libc libkernel) +target_link_libraries(libSceHttp2 PRIVATE libc libkernel anyps5_http2_transport) +if(WIN32) + target_link_libraries(libSceHttp2 PRIVATE ws2_32) +endif() configure_windows_unwind(libSceHttp2) if(BUILD_TESTING) @@ -9,8 +36,18 @@ if(BUILD_TESTING) target_link_libraries(guest_http2_tests PRIVATE libSceHttp2 libkernel libc) configure_windows_unwind(guest_http2_tests) add_test(NAME guest_http2 COMMAND guest_http2_tests) + add_test_executable(guest_http2_transport_tests ${LIBS_INCLUDE_DIR}/tests/GuestHttp2Transport.cpp) + target_include_directories(guest_http2_transport_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_http2_transport_tests PRIVATE libSceHttp2 libSceNet libSceSsl libkernel libc nghttp2_static mbedtls mbedx509 mbedcrypto) + configure_windows_unwind(guest_http2_transport_tests) + add_test(NAME guest_http2_transport COMMAND guest_http2_transport_tests) + add_test(NAME guest_http2_early_response COMMAND guest_http2_transport_tests --early) + set_tests_properties(guest_http2_transport guest_http2_early_response PROPERTIES TIMEOUT 30) if(WIN32) - set_tests_properties(guest_http2 PROPERTIES ENVIRONMENT_MODIFICATION + target_link_libraries(guest_http2_transport_tests PRIVATE ws2_32 bcrypt) + add_test(NAME guest_http2_dns_first COMMAND guest_http2_transport_tests --dns-first) + set_tests_properties(guest_http2_dns_first PROPERTIES TIMEOUT 30) + set_tests_properties(guest_http2 guest_http2_transport guest_http2_early_response guest_http2_dns_first PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceHttp2/Export.cpp b/core/libs/prx/libSceHttp2/Export.cpp index 7cb14fc27a..f10d2f31bf 100644 --- a/core/libs/prx/libSceHttp2/Export.cpp +++ b/core/libs/prx/libSceHttp2/Export.cpp @@ -3,20 +3,15 @@ #include #include #include "SceTypes.hpp" +#include "Transport.hpp" #include "prx/libc/include/General.hpp" -#include "prx/libkernel/Equeue/Equeue.hpp" #include -#include #include #include #include #include #include -#include -// No network is emulated: contexts, templates and requests can be created, but any request -// that would touch the network fails with the library's network error. -static constexpr int ERROR_NETWORK = static_cast(0x80436063); static constexpr int ERROR_INVALID_ID = static_cast(0x817B1100); static constexpr int ERROR_NULL_POINTER = static_cast(0x817B1225); @@ -27,6 +22,12 @@ struct Http2Handle { int owner; std::size_t poolSize; std::shared_ptr cookieMemory; + int sslContext = 0; + int version = 0; + Http2::Settings settings; + std::string method, url; + std::shared_ptr transport; + std::shared_ptr capacity; }; static std::atomic g_nextHandle{1}; @@ -43,305 +44,273 @@ static Http2Handle* FindAnyHandle(int id) { return entry != g_handles.end() ? &entry->second : nullptr; } -static std::vector ReleaseOwnedHandles(int owner) { - std::vector owned; - for (const auto& entry : g_handles) { - if (entry.second.owner == owner) owned.push_back(entry.first); - } - std::vector removed; - for (const int id : owned) { - const std::vector descendants = ReleaseOwnedHandles(id); - removed.insert(removed.end(), descendants.begin(), descendants.end()); +static void ReleaseOwnedHandles(int owner) { + for (;;) { + auto child = g_handles.end(); + for (auto entry = g_handles.begin(); entry != g_handles.end(); ++entry) { + if (entry->second.owner == owner) { child = entry; break; } + } + if (child == g_handles.end()) return; + const int id = child->first; + ReleaseOwnedHandles(id); + auto& handle = g_handles.at(id); + if (handle.transport) handle.transport->Stop(); g_handles.erase(id); - removed.push_back(id); } - return removed; } -static bool HandleIsLive(int id) { +template +static int Update(int id, const char* function, TUpdate update) { std::lock_guard lock(g_handlesMutex); - return FindAnyHandle(id) != nullptr; -} - -static std::mutex g_completionsMutex; -static std::unordered_map> g_completions; - -static void EraseCompletions(const std::vector& ids) { - std::lock_guard lock(g_completionsMutex); - for (const int id : ids) g_completions.erase(id); -} - -static int CompleteAsync(const char* function, int req_id, const Http2AsyncOption* kqueue_option, const void* option) { - if (option != nullptr) NotImplemented_nid_no_patch(function); - bool queued = false; - { - std::lock_guard handlesLock(g_handlesMutex); - if (FindHandle(req_id, Http2Kind::Request) != nullptr) { - std::lock_guard completionsLock(g_completionsMutex); - Http2AsyncResult completion{}; - completion.req_id = req_id; - completion.result = ERROR_NETWORK; - g_completions[req_id].push_back(completion); - queued = true; - } - } - if (!queued) return ERROR_INVALID_ID; - if (kqueue_option == nullptr) return 0; - if (EqueueTriggerEvent_nid_postfix(kqueue_option->equeue, static_cast(kqueue_option->user_event_id), EVFILT_USER, kqueue_option->user_data) != 0) { - { - std::lock_guard lock(g_completionsMutex); - const auto pending = g_completions.find(req_id); - if (pending != g_completions.end() && !pending->second.empty()) pending->second.pop_back(); - } - NotImplemented_nid_no_patch(function); - } + auto* handle = FindAnyHandle(id); + if (!handle) return ERROR_INVALID_ID; + if (handle->kind != Http2Kind::Template && handle->kind != Http2Kind::Request) NotImplemented_nid_no_patch(function); + if (handle->transport) NotImplemented_nid_no_patch(function); + update(handle->settings); return 0; } +static std::shared_ptr Transport(int id, bool create) { + std::lock_guard lock(g_handlesMutex); + auto* handle = FindHandle(id, Http2Kind::Request); + if (!handle) return nullptr; + if (create && !handle->transport) { + if (handle->version != 3) NotImplemented_nid_no_patch("sceHttp2SendRequest: HTTP/1.x transport is not implemented"); + handle->transport = Http2::CreateRequest_nid_no_patch(handle->sslContext, handle->capacity, handle->settings, handle->method, handle->url); + } + if (!handle->transport) NotImplemented_nid_no_patch("sceHttp2: request has not been sent"); + return handle->transport; +} + + extern "C" { int APS5_VABI sceHttp2AddRequestHeader(int id, const char* name, const char* value, uint32_t mode) { - (void)id; - (void)name; - (void)value; - (void)mode; - return 0; + return Update(id, __func__, [&](auto& settings) { Http2::AddHeader(settings, name, value, mode); }); } int APS5_VABI sceHttp2CreateRequestWithURL(int tmpl_id, const char* method, const char* url, uint64_t content_length) { - (void)method; - (void)url; - (void)content_length; std::lock_guard lock(g_handlesMutex); - if (FindHandle(tmpl_id, Http2Kind::Template) == nullptr) return ERROR_INVALID_ID; + const auto* parent = FindHandle(tmpl_id, Http2Kind::Template); + if (!parent) return ERROR_INVALID_ID; + if (!method || !url) NotImplemented_nid_no_patch(__func__); + Http2Handle handle{Http2Kind::Request, tmpl_id, 0}; + handle.sslContext = parent->sslContext; + handle.capacity = parent->capacity; + handle.version = parent->version; + handle.settings = parent->settings; + handle.settings.contentLength = content_length; + handle.method = method; + handle.url = url; const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); - g_handles[id] = {Http2Kind::Request, tmpl_id, 0}; + g_handles.emplace(id, std::move(handle)); return id; } int APS5_VABI sceHttp2CreateTemplate(int lib_http2_ctx_id, const char* user_agent, int http_ver, int is_auto_proxy_conf) { - (void)user_agent; - (void)http_ver; - (void)is_auto_proxy_conf; std::lock_guard lock(g_handlesMutex); - if (FindHandle(lib_http2_ctx_id, Http2Kind::Context) == nullptr) return ERROR_INVALID_ID; + const auto* parent = FindHandle(lib_http2_ctx_id, Http2Kind::Context); + if (!parent) return ERROR_INVALID_ID; + if (!user_agent) NotImplemented_nid_no_patch(__func__); + if ((is_auto_proxy_conf != 0 && is_auto_proxy_conf != 1) || http_ver < 1 || http_ver > 3) NotImplemented_nid_no_patch(__func__); + Http2Handle handle{Http2Kind::Template, lib_http2_ctx_id, 0}; + handle.sslContext = parent->sslContext; + handle.capacity = parent->capacity; + handle.version = http_ver; + handle.settings = parent->settings; + handle.settings.userAgent = user_agent; const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); - g_handles[id] = {Http2Kind::Template, lib_http2_ctx_id, 0}; + g_handles.emplace(id, std::move(handle)); return id; } int APS5_VABI sceHttp2DeleteRequest(int req_id) { - { - std::lock_guard lock(g_handlesMutex); - if (FindHandle(req_id, Http2Kind::Request) == nullptr) return ERROR_INVALID_ID; - g_handles.erase(req_id); - } - EraseCompletions({req_id}); + std::lock_guard lock(g_handlesMutex); + auto* handle = FindHandle(req_id, Http2Kind::Request); + if (!handle) return ERROR_INVALID_ID; + if (handle->transport) handle->transport->Stop(); + g_handles.erase(req_id); return 0; } int APS5_VABI sceHttp2DeleteTemplate(int tmpl_id) { - std::vector removed; - { - std::lock_guard lock(g_handlesMutex); - if (FindHandle(tmpl_id, Http2Kind::Template) == nullptr) return ERROR_INVALID_ID; - removed = ReleaseOwnedHandles(tmpl_id); - g_handles.erase(tmpl_id); - } - EraseCompletions(removed); + std::lock_guard lock(g_handlesMutex); + if (!FindHandle(tmpl_id, Http2Kind::Template)) return ERROR_INVALID_ID; + ReleaseOwnedHandles(tmpl_id); + g_handles.erase(tmpl_id); return 0; } int APS5_VABI sceHttp2GetAllResponseHeaders(int req_id, char** header, size_t* header_size) { - (void)req_id; - (void)header; - (void)header_size; - return ERROR_NETWORK; + if (!Transport(req_id, false)) return ERROR_INVALID_ID; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceHttp2GetResponseContentLength(int req_id, int* result, uint64_t* content_length) { - (void)req_id; - (void)result; - (void)content_length; - return ERROR_NETWORK; + const auto request = Transport(req_id, false); + return request ? request->ContentLength(result, content_length) : ERROR_INVALID_ID; } int APS5_VABI sceHttp2GetStatusCode(int req_id, int* status_code) { - (void)req_id; - (void)status_code; - return ERROR_NETWORK; + const auto request = Transport(req_id, false); + return request ? request->Status(status_code) : ERROR_INVALID_ID; } int APS5_VABI sceHttp2Init(int libnet_mem_id, int libssl_ctx_id, size_t pool_size, int max_concurrent_request) { - (void)libnet_mem_id; - (void)libssl_ctx_id; - (void)max_concurrent_request; + if (max_concurrent_request <= 0) NotImplemented_nid_no_patch(__func__); const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); std::lock_guard lock(g_handlesMutex); - g_handles[id] = {Http2Kind::Context, 0, pool_size}; + Http2Handle handle{Http2Kind::Context, 0, pool_size}; + handle.sslContext = libssl_ctx_id; + handle.capacity = Http2::CreateCapacity_nid_no_patch(max_concurrent_request); + g_handles.emplace(id, std::move(handle)); return id; } int APS5_VABI sceHttp2ReadData(int req_id, void* data, size_t size) { - (void)req_id; - (void)data; - (void)size; - return ERROR_NETWORK; + const auto request = Transport(req_id, false); + return request ? request->Read(data, size) : ERROR_INVALID_ID; } int APS5_VABI sceHttp2ReadDataAsync(int req_id, void* data, size_t size, void* kqueue_option, void* option) { - (void)req_id; - (void)data; - (void)size; - (void)kqueue_option; - (void)option; - return ERROR_NETWORK; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceHttp2SendRequest(int req_id, const void* post_data, size_t size) { - (void)req_id; - (void)post_data; - (void)size; - return ERROR_NETWORK; + const auto request = Transport(req_id, true); + return request ? request->Send(post_data, size) : ERROR_INVALID_ID; } int APS5_VABI sceHttp2SendRequestAsync(int req_id, const void* post_data, size_t size, Http2AsyncOption* kqueue_option, void* option) { - (void)post_data; - (void)size; - return CompleteAsync(__func__, req_id, kqueue_option, option); + std::lock_guard lock(g_handlesMutex); + if (!FindHandle(req_id, Http2Kind::Request)) return ERROR_INVALID_ID; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceHttp2SetAuthEnabled(int id, int is_enable) { - (void)id; - (void)is_enable; - return 0; + return Update(id, __func__, [&](auto& settings) { + if (is_enable != 0 && is_enable != 1) NotImplemented_nid_no_patch("sceHttp2SetAuthEnabled"); + settings.auth = is_enable != 0; + }); } int APS5_VABI sceHttp2SetAutoRedirect(int id, int enable) { - (void)id; - (void)enable; - return 0; + return Update(id, __func__, [&](auto& settings) { + if (enable != 0 && enable != 1) NotImplemented_nid_no_patch("sceHttp2SetAutoRedirect"); + settings.redirect = enable != 0; + }); } int APS5_VABI sceHttp2SetConnectionWaitTimeOut(int id, uint32_t usec) { - (void)id; - (void)usec; + NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceHttp2SetConnectTimeOut(int id, uint32_t usec) { - (void)id; - (void)usec; - return 0; + return Update(id, __func__, [&](auto& settings) { + if (!usec) NotImplemented_nid_no_patch("sceHttp2SetConnectTimeOut"); + settings.connectTimeout = usec; + }); } int APS5_VABI sceHttp2SetInflateGZIPEnabled(int id, int enable) { - (void)id; - (void)enable; - return 0; + return Update(id, __func__, [&](auto&) { if (enable != 0) NotImplemented_nid_no_patch("sceHttp2SetInflateGZIPEnabled"); }); } int APS5_VABI sceHttp2SetRecvTimeOut(int id, uint32_t usec) { - (void)id; - (void)usec; - return 0; + return Update(id, __func__, [&](auto& settings) { + if (!usec) NotImplemented_nid_no_patch("sceHttp2SetRecvTimeOut"); + settings.recvTimeout = usec; + }); } int APS5_VABI sceHttp2SetRedirectCallback(int id, void* cb_func, void* user_arg) { - (void)id; - (void)cb_func; - (void)user_arg; - return 0; + return Update(id, __func__, [&](auto&) { if (cb_func || user_arg) NotImplemented_nid_no_patch("sceHttp2SetRedirectCallback"); }); } int APS5_VABI sceHttp2SetRequestContentLength(int id, uint64_t content_length) { - (void)id; - (void)content_length; - return 0; + return Update(id, __func__, [&](auto& settings) { settings.contentLength = content_length; settings.noContentLength = false; }); } int APS5_VABI sceHttp2SetResolveTimeOut(int id, uint32_t usec) { - (void)id; - (void)usec; - return 0; + return Update(id, __func__, [&](auto& settings) { + if (!usec) NotImplemented_nid_no_patch("sceHttp2SetResolveTimeOut"); + settings.resolveTimeout = usec; + }); } int APS5_VABI sceHttp2SetSendTimeOut(int id, uint32_t usec) { - (void)id; - (void)usec; - return 0; + return Update(id, __func__, [&](auto& settings) { + if (!usec) NotImplemented_nid_no_patch("sceHttp2SetSendTimeOut"); + settings.sendTimeout = usec; + }); } int APS5_VABI sceHttp2SetSslCallback(int id, void* cb_func, void* user_arg) { - (void)id; - (void)cb_func; - (void)user_arg; - return 0; + return Update(id, __func__, [&](auto&) { if (cb_func || user_arg) NotImplemented_nid_no_patch("sceHttp2SetSslCallback"); }); } int APS5_VABI sceHttp2SetTimeOut(int id, uint32_t usec) { - (void)id; - (void)usec; - return 0; + return Update(id, __func__, [&](auto& settings) { + if (!usec) NotImplemented_nid_no_patch("sceHttp2SetTimeOut"); + settings.timeout = usec; + }); } int APS5_VABI sceHttp2SslDisableOption(int id, uint32_t ssl_flags) { - (void)ssl_flags; - return HandleIsLive(id) ? 0 : ERROR_INVALID_ID; + return Update(id, __func__, [&](auto& settings) { + if (ssl_flags & ~0x3du) NotImplemented_nid_no_patch("sceHttp2SslDisableOption"); + settings.verifyFlags &= ~ssl_flags; + }); } int APS5_VABI sceHttp2SslEnableOption(int id, uint32_t ssl_flags) { - (void)ssl_flags; - return HandleIsLive(id) ? 0 : ERROR_INVALID_ID; + return Update(id, __func__, [&](auto& settings) { + if (ssl_flags & ~0x3du) NotImplemented_nid_no_patch("sceHttp2SslEnableOption"); + settings.verifyFlags |= ssl_flags; + }); } int APS5_VABI sceHttp2SetMinSslVersion(int id, uint32_t ssl_version) { - (void)ssl_version; - return HandleIsLive(id) ? 0 : ERROR_INVALID_ID; + return Update(id, __func__, [&](auto&) { NotImplemented_nid_no_patch("sceHttp2SetMinSslVersion"); }); } int APS5_VABI sceHttp2Term(int lib_http2_ctx_id) { - std::vector removed; - { - std::lock_guard lock(g_handlesMutex); - if (FindHandle(lib_http2_ctx_id, Http2Kind::Context) == nullptr) return ERROR_INVALID_ID; - removed = ReleaseOwnedHandles(lib_http2_ctx_id); - g_handles.erase(lib_http2_ctx_id); - } - EraseCompletions(removed); + std::lock_guard lock(g_handlesMutex); + if (!FindHandle(lib_http2_ctx_id, Http2Kind::Context)) return ERROR_INVALID_ID; + ReleaseOwnedHandles(lib_http2_ctx_id); + g_handles.erase(lib_http2_ctx_id); return 0; } int APS5_VABI sceHttp2WaitAsync(int req_id, Http2AsyncResult* result, uint32_t* timeout, void* option) { - (void)timeout; - if (result == nullptr || option != nullptr) NotImplemented_nid_no_patch(__func__); - std::lock_guard lock(g_completionsMutex); - const auto pending = g_completions.find(req_id); - if (pending == g_completions.end() || pending->second.empty()) NotImplemented_nid_no_patch("sceHttp2WaitAsync: waiting for an operation that has not completed"); - *result = pending->second.front(); - pending->second.pop_front(); + NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceHttp2AbortRequest(int req_id) { - (void)req_id; + const auto request = Transport(req_id, false); + if (!request) return ERROR_INVALID_ID; + request->Cancel(); return 0; } int APS5_VABI sceHttp2GetMemoryPoolStats(int lib_http2_ctx_id, Http2MemoryPoolStats* stats) { if (stats == nullptr) return ERROR_NULL_POINTER; std::lock_guard lock(g_handlesMutex); - const Http2Handle* handle = FindHandle(lib_http2_ctx_id, Http2Kind::Context); - if (handle == nullptr) { + if (!FindHandle(lib_http2_ctx_id, Http2Kind::Context)) { std::memset(stats, 0, sizeof(*stats)); return ERROR_INVALID_ID; } - *stats = {handle->poolSize, 0, 0, 0}; + NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceHttp2CookieFlush(int id) { - (void)id; + NotImplemented_nid_no_patch(__func__); return 0; } @@ -352,32 +321,27 @@ int APS5_VABI sceHttp2CreateCookieBox(std::size_t poolSize) { auto memory = std::make_shared(allocationSize); std::lock_guard lock(g_handlesMutex); const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); - g_handles[id] = {Http2Kind::CookieBox, 0, poolSize, std::move(memory)}; + g_handles.emplace(id, Http2Handle{Http2Kind::CookieBox, 0, poolSize, std::move(memory)}); return id; } int APS5_VABI sceHttp2DeleteCookieBox(int cookie_box_id) { std::lock_guard lock(g_handlesMutex); - if (FindHandle(cookie_box_id, Http2Kind::CookieBox) == nullptr) return ERROR_INVALID_ID; + if (!FindHandle(cookie_box_id, Http2Kind::CookieBox)) return ERROR_INVALID_ID; g_handles.erase(cookie_box_id); return 0; } int APS5_VABI sceHttp2SetCookieBox(int id, int cookie_box_id) { - (void)id; - (void)cookie_box_id; - return 0; + return Update(id, __func__, [&](auto&) { if (cookie_box_id != 0) NotImplemented_nid_no_patch("sceHttp2SetCookieBox"); }); } int APS5_VABI sceHttp2SetRequestNoContentLength(int id) { - (void)id; - return 0; + return Update(id, __func__, [&](auto& settings) { settings.noContentLength = true; }); } int APS5_VABI sceHttp2SetResolveRetry(int id, int32_t retry) { - (void)id; - (void)retry; - return 0; + return Update(id, __func__, [&](auto&) { if (retry != 0) NotImplemented_nid_no_patch("sceHttp2SetResolveRetry"); }); } int APS5_VABI sceHttp2WebSocketCreateRequest(std::int32_t templateId, const char* url, Http2WebSocketTextCallback textCallback, Http2WebSocketDataCallback dataCallback, Http2WebSocketCloseCallback closeCallback, Http2WebSocketErrorCallback errorCallback, void* receiveBuffer, std::size_t receiveBufferSize, void* userData, const void* option) { diff --git a/core/libs/prx/libSceHttp2/Transport.cpp b/core/libs/prx/libSceHttp2/Transport.cpp new file mode 100644 index 0000000000..767e89579e --- /dev/null +++ b/core/libs/prx/libSceHttp2/Transport.cpp @@ -0,0 +1,632 @@ +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#endif + +#include "Transport.hpp" +#include "prx/libSceNet/include/Net.hpp" +#include "prx/libSceSsl/include/Ssl.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/Socket/include/SocketPoll.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" int SslRead_nid_no_patch(int, void*, std::size_t, int); + +namespace Http2 { +namespace { + +using Clock = std::chrono::steady_clock; +constexpr int WantRead = static_cast(0x8095f013); +constexpr int WantWrite = static_cast(0x8095f014); + +[[noreturn]] void Unknown(const char* message) { + NotImplemented_nid_no_patch(message); + std::terminate(); +} + +void Require(bool condition, const char* message) { + if (!condition) Unknown(message); +} + +std::string Lower(std::string text) { + for (auto& c : text) if (c >= 'A' && c <= 'Z') c += 'a' - 'A'; + return text; +} + +std::uint64_t Number(std::string_view text) { + std::uint64_t value = 0; + const auto result = std::from_chars(text.data(), text.data() + text.size(), value); + Require(!text.empty() && result.ec == std::errc{} && result.ptr == text.data() + text.size(), + "sceHttp2: invalid number; console error mapping is unknown"); + return value; +} + +struct Url { + std::string host, authority, path; + std::uint16_t port = 443; +}; + +Url Parse(std::string_view text) { + Require(text.size() >= 8 && Lower(std::string(text.substr(0, 8))) == "https://", + "sceHttp2: only HTTPS HTTP/2 transport is implemented"); + for (const unsigned char c : text) Require(c > 0x20 && c != 0x7f, "sceHttp2: invalid URL character"); + text.remove_prefix(8); + const auto end = text.find_first_of("/?#"); + const auto authority = text.substr(0, end); + Require(!authority.empty() && authority.find('@') == std::string_view::npos, "sceHttp2: empty authority or URL credentials"); + Url url; + url.authority = authority; + auto host = authority; + std::string_view port; + if (host.starts_with('[')) { + const auto closing = host.find(']'); + Require(closing != std::string_view::npos, "sceHttp2: invalid IPv6 authority"); + if (closing + 1 != host.size()) { + Require(host[closing + 1] == ':', "sceHttp2: invalid authority suffix"); + port = host.substr(closing + 2); + Require(!port.empty(), "sceHttp2: empty URL port"); + } + host = host.substr(1, closing - 1); + } else if (const auto colon = host.find(':'); colon != std::string_view::npos) { + port = host.substr(colon + 1); + Require(!port.empty() && port.find(':') == std::string_view::npos, "sceHttp2: invalid URL port"); + host = host.substr(0, colon); + } + Require(!host.empty(), "sceHttp2: empty host"); + url.host = host; + if (!port.empty()) { + const auto value = Number(port); + Require(value > 0 && value <= 65535, "sceHttp2: invalid URL port"); + url.port = static_cast(value); + } + text = end == std::string_view::npos ? std::string_view{} : text.substr(end); + text = text.substr(0, text.find('#')); + url.path = text.starts_with('/') ? std::string(text) : "/" + std::string(text); + return url; +} + +struct Address { + std::array bytes{}; + std::uint32_t size = 0; +}; + +struct Lookup { + std::string host; + std::uint16_t port; + std::mutex mutex; + std::condition_variable changed; + bool complete = false; + std::vector
addresses; + std::exception_ptr failure; +}; + +class Resolver { +public: + Resolver() { + try { + for (auto& thread : workers) thread = std::thread([this] { Work(); }); + } catch (...) { Stop(); throw; } + } + ~Resolver() { Stop(); } + std::shared_ptr Submit(const Url& url) { + auto job = std::make_shared(); + job->host = url.host; + job->port = url.port; + { + std::lock_guard lock(mutex); + Require(queue.size() < 8, "sceHttp2: host DNS worker queue exhausted"); + queue.push_back(job); + } + changed.notify_one(); + return job; + } +private: + void Stop() { + { + std::lock_guard lock(mutex); + stopping = true; + } + changed.notify_all(); + for (auto& thread : workers) if (thread.joinable()) thread.join(); + } + void Work() { + for (;;) { + std::shared_ptr job; + { + std::unique_lock lock(mutex); + changed.wait(lock, [&] { return stopping || !queue.empty(); }); + if (stopping) return; + job = std::move(queue.front()); + queue.pop_front(); + } + try { + addrinfo hints{}; + hints.ai_socktype = SOCK_STREAM; + hints.ai_family = AF_UNSPEC; + addrinfo* raw = nullptr; + const auto service = std::to_string(job->port); + const int result = getaddrinfo(job->host.c_str(), service.c_str(), &hints, &raw); + std::unique_ptr addresses(raw, freeaddrinfo); + Require(result == 0, "sceHttp2: DNS failed; console error mapping is unknown"); + for (auto* entry = raw; entry; entry = entry->ai_next) { + Address address; + if (entry->ai_family == AF_INET) { + address.size = 16; + address.bytes[1] = 2; + const auto* ipv4 = reinterpret_cast(entry->ai_addr); + std::memcpy(address.bytes.data() + 2, &ipv4->sin_port, 2); + std::memcpy(address.bytes.data() + 4, &ipv4->sin_addr, 4); + } else if (entry->ai_family == AF_INET6) { + address.size = 28; + address.bytes[1] = 28; + const auto* ipv6 = reinterpret_cast(entry->ai_addr); + std::memcpy(address.bytes.data() + 2, &ipv6->sin6_port, 2); + std::memcpy(address.bytes.data() + 4, &ipv6->sin6_flowinfo, 4); + std::memcpy(address.bytes.data() + 8, &ipv6->sin6_addr, 16); + std::memcpy(address.bytes.data() + 24, &ipv6->sin6_scope_id, 4); + } else continue; + address.bytes[0] = static_cast(address.size); + job->addresses.push_back(address); + } + Require(!job->addresses.empty(), "sceHttp2: DNS returned no supported address"); + } catch (...) { + job->failure = std::current_exception(); + } + { + std::lock_guard lock(job->mutex); + job->complete = true; + } + job->changed.notify_all(); + } + } + std::mutex mutex; + std::condition_variable changed; + std::deque> queue; + bool stopping = false; + std::array workers; +}; + +Resolver& Dns() { + static Resolver resolver; + return resolver; +} + +void CheckNg(int result) { + if (result == NGHTTP2_ERR_NOMEM) throw std::bad_alloc(); + Require(result >= 0, "sceHttp2: HTTP/2 protocol failure; console error mapping is unknown"); +} + +} + +void AddHeader(Settings& settings, const char* name, const char* value, std::uint32_t mode) { + Require(name && value && mode <= 1, "sceHttp2AddRequestHeader: unsupported arguments"); + auto key = Lower(name); + Require(!key.empty() && nghttp2_check_header_name(reinterpret_cast(key.data()), key.size()), + "sceHttp2AddRequestHeader: invalid header name"); + Require(nghttp2_check_header_value_rfc9113(reinterpret_cast(value), std::strlen(value)), + "sceHttp2AddRequestHeader: invalid header value"); + Require(key != "connection" && key != "proxy-connection" && key != "keep-alive" && key != "upgrade" && + key != "transfer-encoding" && key != "host" && 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; }); + updated.emplace_back(std::move(key), value); + settings.headers = std::move(updated); +} + +struct Request::State { + int sslContext; + const std::shared_ptr capacity; + bool acquired = false; + const Settings settings; + const std::string method; + const Url url; + std::mutex mutex; + std::atomic cancelled{false}; + bool started = false; + bool ready = false; + bool ended = false; + int status = 0; + int blockStatus = 0; + int networkResult = 0; + int socket = -1; + int ssl = -1; + std::uintptr_t native = 0; + std::shared_ptr socketOwner; + nghttp2_session* session = nullptr; + std::int32_t stream = -1; + std::uint64_t contentLength = 0; + bool hasLength = false; + std::size_t headerBytes = 0; + std::vector body; + std::size_t bodyOffset = 0; + const std::uint8_t* upload = nullptr; + std::size_t uploadSize = 0; + std::size_t uploadOffset = 0; + std::exception_ptr failure; + std::exception_ptr callbackFailure; + Clock::time_point totalDeadline; + + State(int context, std::shared_ptr capacity, Settings options, std::string verb, std::string target) + : sslContext(context), capacity(std::move(capacity)), settings(std::move(options)), method(std::move(verb)), url(Parse(target)) { + Require(!method.empty() && nghttp2_check_method(reinterpret_cast(method.data()), method.size()), + "sceHttp2CreateRequestWithURL: invalid method"); + Require(method != "CONNECT", "sceHttp2: CONNECT is not implemented"); + Require(nghttp2_check_header_value_rfc9113(reinterpret_cast(settings.userAgent.data()), settings.userAgent.size()), + "sceHttp2: invalid user agent"); + } + + ~State() { Close(); } + + void Close() { + if (session) nghttp2_session_del(std::exchange(session, nullptr)); + if (ssl >= 0) sceSslDeleteConnection(std::exchange(ssl, -1)); + if (socket >= 0) sceNetSocketClose(std::exchange(socket, -1)); + socketOwner.reset(); + if (std::exchange(acquired, false)) capacity->active.fetch_sub(1); + } + + Clock::time_point Deadline(std::uint32_t duration) const { + Require(duration != 0, "sceHttp2: zero timeout semantics are unknown"); + return std::min(totalDeadline, Clock::now() + std::chrono::microseconds(duration)); + } + + void Check(Clock::time_point deadline) const { + Require(!cancelled.load(), "sceHttp2: request cancelled; console error mapping is unknown"); + Require(Clock::now() < deadline, "sceHttp2: request timed out; console error mapping is unknown"); + } + + void Wait(bool write, Clock::time_point deadline) { + for (;;) { + Check(deadline); +#ifdef _WIN32 + WSAPOLLFD entry{static_cast(native), static_cast(write ? POLLWRNORM : POLLRDNORM), 0}; + const auto result = WSAPoll(&entry, 1, 5); +#else + pollfd entry{static_cast(native), static_cast(write ? POLLOUT : POLLIN), 0}; + const auto result = poll(&entry, 1, 5); +#endif + Require(result >= 0, "sceHttp2: host poll failed"); + if (result > 0) { Check(deadline); return; } + } + } + + int Connect() { +#ifdef _WIN32 + const int startup = sceNetSocket("http2-resolver", 2, 1, 0); + Require(startup >= 0, "sceHttp2: network initialization failed; console error mapping is unknown"); + Require(sceNetSocketClose(startup) == 0, "sceHttp2: network initialization socket close failed"); +#endif + const auto deadline = Deadline(settings.resolveTimeout); + const auto job = Dns().Submit(url); + { + std::unique_lock lock(job->mutex); + while (!job->complete) { + Check(deadline); + job->changed.wait_for(lock, std::chrono::milliseconds(5)); + } + Check(deadline); + if (job->failure) std::rethrow_exception(job->failure); + } + const auto connectDeadline = Deadline(settings.connectTimeout); + int result = 0; + for (const auto& address : job->addresses) { + Check(connectDeadline); + socket = sceNetSocket("http2", address.bytes[1], 1, 0); + Require(socket >= 0, "sceHttp2: socket creation failed; console error mapping is unknown"); + int enabled = 1; + Require(sceNetSetsockopt(socket, 0xffff, 0x1200, &enabled, sizeof(enabled)) == 0, + "sceHttp2: cannot set nonblocking socket"); + Require(KernelLookupSocketNative_nid_no_patch(socket, &socketOwner, &native), "sceHttp2: native socket lookup failed"); + result = sceNetConnect(socket, address.bytes.data(), address.size); + if (result < 0 && *sceNetErrnoLoc() == 36) { + Wait(true, connectDeadline); + int error = 0; + std::uint32_t size = sizeof(error); + result = sceNetGetsockopt(socket, 0xffff, 0x1007, &error, &size); + Require(result == 0, "sceHttp2: connect status unavailable; console error mapping is unknown"); + if (result == 0 && error != 0) result = static_cast(0x80410100u | static_cast(error)); + } + if (result == 0) break; + sceNetSocketClose(std::exchange(socket, -1)); + socketOwner.reset(); + } + if (result < 0) return result; + ssl = sceSslCreateConnection(sslContext, socket, url.host.c_str()); + Require(ssl > 0, "sceHttp2: SSL context or connection unavailable"); + Require(sceSslDisableVerifyOption(ssl, 0x3d) == 0 && sceSslEnableVerifyOption(ssl, settings.verifyFlags) == 0, + "sceHttp2: SSL verification options rejected"); + Require(sceSslSetAlpn(ssl, "h2") == 0, "sceHttp2: ALPN setup failed"); + for (;;) { + Check(connectDeadline); + result = sceSslConnect(ssl); + if (result == WantRead || result == WantWrite) Wait(result == WantWrite, connectDeadline); + else break; + } + Require(result == 0, "sceHttp2: TLS failed; console error mapping is unknown"); + const char* selected = sceSslGetAlpnSelected(ssl); + Require(selected && std::string_view(selected) == "h2", "sceHttp2: server did not negotiate h2; fallback contract is unknown"); + return 0; + } + + template + static int Callback(State& state, TCallback callback) noexcept { + try { callback(); return 0; } + catch (...) { state.callbackFailure = std::current_exception(); return NGHTTP2_ERR_CALLBACK_FAILURE; } + } + + static int BeginHeaders(nghttp2_session*, const nghttp2_frame* frame, void* data) { + auto& state = *static_cast(data); + return Callback(state, [&] { + if (frame->hd.stream_id == state.stream) { + Require(!state.ready, "sceHttp2: response trailers are not implemented"); + state.blockStatus = 0; + state.hasLength = false; + state.contentLength = 0; + } + }); + } + + static int Header(nghttp2_session*, const nghttp2_frame* frame, const std::uint8_t* name, std::size_t nameSize, + const std::uint8_t* value, std::size_t valueSize, std::uint8_t, void* data) { + auto& state = *static_cast(data); + return Callback(state, [&] { + if (frame->hd.stream_id != state.stream) return; + Require(nameSize <= 65536 && valueSize <= 65536 && nameSize + valueSize <= 65536 - state.headerBytes, + "sceHttp2: response headers exceed host limit"); + state.headerBytes += nameSize + valueSize; + const std::string_view key(reinterpret_cast(name), nameSize); + const std::string_view text(reinterpret_cast(value), valueSize); + if (key == ":status") state.blockStatus = static_cast(Number(text)); + if (key == "content-length") { + state.contentLength = Number(text); + state.hasLength = true; + } + }); + } + + static int Frame(nghttp2_session*, const nghttp2_frame* frame, void* data) { + auto& state = *static_cast(data); + return Callback(state, [&] { + if (frame->hd.type == NGHTTP2_GOAWAY) + Require(frame->goaway.error_code == NGHTTP2_NO_ERROR && frame->goaway.last_stream_id >= state.stream, + "sceHttp2: GOAWAY rejected request"); + if (frame->hd.stream_id != state.stream) return; + if (frame->hd.type == NGHTTP2_HEADERS && state.blockStatus >= 200) { + state.status = state.blockStatus; + Require(!state.settings.redirect || (state.status != 301 && state.status != 302 && state.status != 303 && state.status != 307 && state.status != 308), + "sceHttp2: automatic redirects are not implemented"); + Require(!state.settings.auth || (state.status != 401 && state.status != 407), "sceHttp2: authentication is not implemented"); + state.ready = true; + } + if ((frame->hd.type == NGHTTP2_HEADERS || frame->hd.type == NGHTTP2_DATA) && + (frame->hd.flags & NGHTTP2_FLAG_END_STREAM)) { + Require(state.ready, "sceHttp2: response ended without final headers"); + state.ended = true; + } + }); + } + + static int Data(nghttp2_session*, std::uint8_t, std::int32_t streamId, const std::uint8_t* bytes, std::size_t size, void* data) { + auto& state = *static_cast(data); + return Callback(state, [&] { + if (streamId != state.stream) return; + Require(state.body.size() + size <= 65535, "sceHttp2: receive flow-control limit exceeded"); + state.body.insert(state.body.end(), bytes, bytes + size); + }); + } + + static int End(nghttp2_session*, std::int32_t streamId, std::uint32_t error, void* data) { + auto& state = *static_cast(data); + return Callback(state, [&] { + if (streamId != state.stream) return; + Require(error == NGHTTP2_NO_ERROR && state.ended, "sceHttp2: stream failed or response is incomplete"); + state.ended = true; + }); + } + + static nghttp2_ssize Upload(nghttp2_session*, std::int32_t, std::uint8_t* buffer, std::size_t size, + std::uint32_t* flags, nghttp2_data_source*, void* data) { + auto& state = *static_cast(data); + if (state.ready) return NGHTTP2_ERR_DEFERRED; + const auto count = std::min(size, state.uploadSize - state.uploadOffset); + if (count) std::memcpy(buffer, state.upload + state.uploadOffset, count); + state.uploadOffset += count; + if (state.uploadOffset == state.uploadSize) *flags |= NGHTTP2_DATA_FLAG_EOF; + return static_cast(count); + } + + void Setup(const void* data, std::size_t size) { + nghttp2_session_callbacks* raw = nullptr; + CheckNg(nghttp2_session_callbacks_new(&raw)); + std::unique_ptr callbacks(raw, nghttp2_session_callbacks_del); + nghttp2_session_callbacks_set_on_begin_headers_callback(raw, BeginHeaders); + nghttp2_session_callbacks_set_on_header_callback(raw, Header); + nghttp2_session_callbacks_set_on_frame_recv_callback(raw, Frame); + nghttp2_session_callbacks_set_on_data_chunk_recv_callback(raw, Data); + nghttp2_session_callbacks_set_on_stream_close_callback(raw, End); + nghttp2_option* optionsRaw = nullptr; + CheckNg(nghttp2_option_new(&optionsRaw)); + std::unique_ptr options(optionsRaw, nghttp2_option_del); + nghttp2_option_set_no_auto_window_update(optionsRaw, 1); + 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::vector> fields{ + {":method", method}, {":scheme", "https"}, {":authority", url.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()); + 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()), + reinterpret_cast(value.data()), name.size(), value.size(), NGHTTP2_NV_FLAG_NONE}); + upload = static_cast(data); + uploadSize = size; + nghttp2_data_provider2 provider{}; + provider.read_callback = Upload; + stream = nghttp2_submit_request2(session, nullptr, headers.data(), headers.size(), size ? &provider : nullptr, nullptr); + CheckNg(stream); + } + + void Flush(Clock::time_point deadline) { + for (;;) { + const std::uint8_t* bytes = nullptr; + const auto size = nghttp2_session_mem_send2(session, &bytes); + CheckNg(static_cast(size)); + if (!size) return; + std::size_t offset = 0; + while (offset < static_cast(size)) { + Check(deadline); + const int result = sceSslWrite(ssl, const_cast(bytes + offset), static_cast(size) - offset, 0); + if (result == WantRead || result == WantWrite) { Wait(result == WantWrite, deadline); continue; } + Require(result > 0, "sceHttp2: TLS write failed; console error mapping is unknown"); + offset += static_cast(result); + } + } + } + + void Pump(Clock::time_point deadline) { + Flush(std::min(deadline, Deadline(settings.sendTimeout))); + Receive(deadline); + } + + void Receive(Clock::time_point deadline) { + std::array buffer; + for (;;) { + Check(deadline); + const int size = SslRead_nid_no_patch(ssl, buffer.data(), buffer.size(), 0); + if (size == WantRead || size == WantWrite) { Wait(size == WantWrite, deadline); continue; } + Require(size > 0, "sceHttp2: TLS read failed or response truncated; console error mapping is unknown"); + const auto consumed = nghttp2_session_mem_recv2(session, buffer.data(), static_cast(size)); + if (callbackFailure) std::rethrow_exception(callbackFailure); + CheckNg(static_cast(consumed)); + Require(consumed == size, "sceHttp2: incomplete HTTP/2 parser consumption"); + return; + } + } + + std::unique_lock Lock() { + std::unique_lock lock(mutex, std::try_to_lock); + Require(lock.owns_lock(), "sceHttp2: concurrent operation on one request is not implemented"); + if (failure) std::rethrow_exception(failure); + Require(!cancelled.load(), "sceHttp2: request cancelled; console error mapping is unknown"); + return lock; + } + + void Fail() { + failure = std::current_exception(); + upload = nullptr; + Close(); + } +}; + +Request::Request(int context, std::shared_ptr capacity, Settings settings, std::string method, std::string url) + : state(std::make_unique(context, std::move(capacity), std::move(settings), std::move(method), std::move(url))) {} +Request::~Request() = default; + +int Request::Send(const void* data, std::size_t size) { + auto lock = state->Lock(); + Require(!state->started, "sceHttp2SendRequest: request reuse is not implemented"); + Require(data || size == 0, "sceHttp2SendRequest: null body"); + Require(state->settings.noContentLength || size == state->settings.contentLength, "sceHttp2SendRequest: body length mismatch"); + state->started = true; + state->totalDeadline = Clock::now() + std::chrono::microseconds(state->settings.timeout); + try { + const int active = state->capacity->active.fetch_add(1); + state->acquired = true; + Require(active < state->capacity->maximum, "sceHttp2: connection slot waiting is not implemented"); + state->networkResult = state->Connect(); + if (state->networkResult < 0) { state->Close(); return state->networkResult; } + state->Setup(data, size); + const auto sendDeadline = state->Deadline(state->settings.sendTimeout); + auto receiveDeadline = sendDeadline; + bool uploaded = false; + for (;;) { + state->Flush(uploaded ? state->Deadline(state->settings.sendTimeout) : sendDeadline); + if (!uploaded && state->uploadOffset == size) { + uploaded = true; + receiveDeadline = state->Deadline(state->settings.recvTimeout); + } + if (state->ready) break; + state->Receive(uploaded ? receiveDeadline : sendDeadline); + if (state->ready) break; + } + state->upload = nullptr; + if (state->ended && state->body.empty()) state->Close(); + return 0; + } catch (...) { state->Fail(); throw; } +} + +int Request::Read(void* data, std::size_t size) { + auto lock = state->Lock(); + if (state->networkResult) return state->networkResult; + Require(state->ready, "sceHttp2ReadData: response unavailable"); + Require(data || size == 0, "sceHttp2ReadData: null buffer"); + if (!size) return 0; + if (state->ended && state->body.empty()) return 0; + try { + const auto deadline = state->Deadline(state->settings.recvTimeout); + state->Check(deadline); + while (state->bodyOffset == state->body.size() && !state->ended) state->Pump(deadline); + const auto count = std::min({size, state->body.size() - state->bodyOffset, static_cast(std::numeric_limits::max())}); + state->Check(deadline); + if (count) std::memcpy(data, state->body.data() + state->bodyOffset, count); + state->bodyOffset += count; + if (state->bodyOffset == state->body.size()) { state->body.clear(); state->bodyOffset = 0; } + if (count && !state->ended) CheckNg(nghttp2_session_consume(state->session, state->stream, count)); + if (state->ended && state->body.empty()) state->Close(); + return static_cast(count); + } catch (...) { state->Fail(); throw; } +} + +int Request::Status(int* status) { + auto lock = state->Lock(); + if (state->networkResult) return state->networkResult; + Require(status && state->ready, "sceHttp2GetStatusCode: null output or response unavailable"); + *status = state->status; + return 0; +} + +int Request::ContentLength(int* kind, std::uint64_t* length) { + auto lock = state->Lock(); + if (state->networkResult) return state->networkResult; + Require(kind && length && state->ready, "sceHttp2GetResponseContentLength: null output or response unavailable"); + *kind = state->hasLength ? 0 : 1; + *length = state->hasLength ? state->contentLength : 0; + return 0; +} + +void Request::Cancel() { state->cancelled.store(true); } + +void Request::Stop() { + Cancel(); + std::lock_guard lock(state->mutex); + state->Close(); +} + +std::shared_ptr CreateCapacity_nid_no_patch(int maximum) { + return std::make_shared(maximum); +} + +std::shared_ptr CreateRequest_nid_no_patch(int sslContext, std::shared_ptr capacity, Settings settings, std::string method, std::string url) { + return std::make_shared(sslContext, std::move(capacity), std::move(settings), std::move(method), std::move(url)); +} + +} diff --git a/core/libs/prx/libSceHttp2/Transport.hpp b/core/libs/prx/libSceHttp2/Transport.hpp new file mode 100644 index 0000000000..5549358bf5 --- /dev/null +++ b/core/libs/prx/libSceHttp2/Transport.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace Http2 { + +struct Settings { + std::string userAgent; + std::vector> headers; + std::uint64_t contentLength = 0; + bool noContentLength = false; + bool auth = true; + bool redirect = true; + std::uint32_t verifyFlags = 0x3d; + std::uint32_t timeout = 120000000; + std::uint32_t connectTimeout = 30000000; + std::uint32_t sendTimeout = 120000000; + std::uint32_t recvTimeout = 120000000; + std::uint32_t resolveTimeout = 30000000; +}; + +struct Capacity { + explicit Capacity(int maximum) : maximum(maximum) {} + const int maximum; + std::atomic active{0}; +}; + +class Request { +public: + Request(int sslContext, std::shared_ptr capacity, Settings settings, std::string method, std::string url); + ~Request(); + Request(const Request&) = delete; + Request& operator=(const Request&) = delete; + int Send(const void* data, std::size_t size); + int Read(void* data, std::size_t size); + int Status(int* status); + int ContentLength(int* kind, std::uint64_t* length); + void Cancel(); + void Stop(); +private: + struct State; + std::unique_ptr state; +}; + +void AddHeader(Settings& settings, const char* name, const char* value, std::uint32_t mode); +std::shared_ptr CreateCapacity_nid_no_patch(int maximum); +std::shared_ptr CreateRequest_nid_no_patch(int sslContext, std::shared_ptr capacity, Settings settings, std::string method, std::string url); + +} diff --git a/core/libs/prx/libSceHttpCache/CMakeLists.txt b/core/libs/prx/libSceHttpCache/CMakeLists.txt new file mode 100644 index 0000000000..da7dc5c182 --- /dev/null +++ b/core/libs/prx/libSceHttpCache/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceHttpCache SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceHttpCache PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceHttpCache) +target_link_libraries(libSceHttpCache PRIVATE libSceUserService libSceRtc) + +if(BUILD_TESTING) + add_test_executable(guest_http_cache_tests ${LIBS_INCLUDE_DIR}/tests/GuestHttpCache.cpp) + target_include_directories(guest_http_cache_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_http_cache_tests PRIVATE libSceHttpCache libSceUserService libSceRtc libc) + configure_windows_unwind(guest_http_cache_tests) + add_test(NAME guest_http_cache COMMAND guest_http_cache_tests) + if(WIN32) + set_tests_properties(guest_http_cache PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$;PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceHttpCache/Export.cpp b/core/libs/prx/libSceHttpCache/Export.cpp index d3bdbf753f..935ad68d53 100644 --- a/core/libs/prx/libSceHttpCache/Export.cpp +++ b/core/libs/prx/libSceHttpCache/Export.cpp @@ -1,20 +1,728 @@ -#include "prx/libSceHttpCache/include/Api.hpp" +#include "prx/libSceHttpCache/libSceHttpCache.hpp" +#include +#include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "SceTypes.hpp" +#include "prx/libSceRtc/libSceRtc.hpp" +#include "prx/libSceUserService/libSceUserService.hpp" #include "prx/libc/include/General.hpp" static_assert(sizeof(HttpCacheHeader) == 16); static_assert(sizeof(HttpCacheRequest) == 32); static_assert(offsetof(HttpCacheRequest, url) == 8); +static_assert(offsetof(HttpCacheRequest, headerCount) == 16); static_assert(offsetof(HttpCacheRequest, headers) == 24); static_assert(sizeof(HttpCacheResponseHeader) == 24); +static_assert(offsetof(HttpCacheResponseHeader, headerCount) == 8); static_assert(offsetof(HttpCacheResponseHeader, headers) == 16); static_assert(sizeof(HttpCacheResponse) == 32); +static_assert(offsetof(HttpCacheResponse, headerCount) == 8); +static_assert(offsetof(HttpCacheResponse, headers) == 16); static_assert(offsetof(HttpCacheResponse, contentLength) == 24); static_assert(sizeof(HttpCacheQuotaEntry) == 16); static_assert(sizeof(HttpCacheQuota) == 16); +namespace { + +constexpr int ErrorUnknown = static_cast(0x817a0000); +constexpr int ErrorNoMemory = static_cast(0x817a0001); +constexpr int ErrorTooManyCaches = static_cast(0x817a0002); +constexpr int ErrorNotFound = static_cast(0x817a0011); +constexpr int ErrorInvalidValue = static_cast(0x817a0012); +constexpr int ErrorInvalidUser = static_cast(0x817a0013); +constexpr int UserServiceAlreadyInitialized = static_cast(0x80960003); +constexpr int UserServiceNoMemory = static_cast(0x80960004); +constexpr int UserServiceInvalidArgument = static_cast(0x80960005); +constexpr std::size_t MaxCaches = 127; +constexpr std::uint32_t RequestCounterLimit = 0x1000000; +constexpr std::int32_t SharedUser = 0xfe; +constexpr std::uint64_t HeuristicWarningSeconds = 86400; + +constexpr int ResultMiss = 0; +constexpr int ResultHit = 1; +constexpr int ResultRevalidate = 2; + +enum class State { + Idle, + Miss, + Hit, + Revalidate, + RevalidateWithStale, + Replacing, + Validated, + Dropped, +}; + +constexpr std::string_view StaleWarning = "110 - \"Response is Stale\""; +constexpr std::string_view HeuristicWarning = "113 - \"Heuristic expiration\""; + +constexpr std::array UnmergedHeaders{ + "allow", "connection", "etag", "keep-alive", "last-modified", "proxy-authenticate", "proxy-connection", + "trailer", "transfer-encoding", "upgrade", "www-authenticate", "x-frame-options", "x-xss-protection"}; +constexpr std::array UnmergedHeaderNames{"content-", "x-content-", "x-webkit-"}; +constexpr std::array ConditionalHeaders{ + "If-Match", "If-Modified-Since", "If-None-Match", "If-Range", "If-Unmodified-Since"}; + +struct Header { + std::string name; + std::string value; +}; + +using Headers = std::vector
; + +struct CacheControl { + bool present = false; + bool maxAge = false; + std::uint64_t maxAgeTicks = 0; + bool sMaxage = false; + std::uint64_t sMaxageTicks = 0; + bool maxStale = false; + bool maxStaleLimited = false; + std::uint64_t maxStaleTicks = 0; + bool staleWhileRevalidate = false; + std::uint64_t staleWhileRevalidateTicks = 0; + bool staleIfError = false; + std::uint64_t staleIfErrorTicks = 0; + bool noCache = false; + bool noStore = false; + bool mustRevalidate = false; + bool proxyRevalidate = false; + bool isPrivate = false; + bool isPublic = false; +}; + +using StoreKey = std::tuple; + +struct Entry { + std::uint32_t status = 0; + Headers headers; + std::uint64_t responseTime = 0; + std::uint64_t lifetime = 0; + Headers vary; + std::string body; + bool isPublic = false; + bool pending = false; + std::size_t readOffset = 0; + StoreKey key; +}; + +struct RequestData { + std::uint32_t method = 0; + std::string url; + Headers headers; +}; + +struct ResponseData { + std::uint32_t status = 0; + Headers headers; + std::uint64_t time = 0; +}; + +struct Request { + std::int32_t storeUser = -1; + bool hasStore = false; + State state = State::Idle; + int lastResult = ResultMiss; + std::optional current; + std::optional next; + std::shared_ptr entry; + std::deque strings; + std::deque> headerArrays; +}; + +struct Cache { + std::uint32_t nextRequest = 1; + std::map requests; +}; + +std::mutex Mutex; +std::map Caches; +std::map Store; +std::int32_t LastCacheId = 0; +bool UserServiceStarted = false; +std::uint8_t CacheSharing = 1; + +Cache* FindCache(std::int32_t cacheId) { + const auto found = Caches.find(cacheId); + return found == Caches.end() ? nullptr : &found->second; +} + +Cache* RequestCache(std::int32_t requestId) { + return FindCache(static_cast(static_cast(requestId) >> 24)); +} + +Request* FindRequest(std::int32_t requestId) { + Cache* cache = RequestCache(requestId); + if (cache == nullptr) return nullptr; + const auto found = cache->requests.find(requestId); + return found == cache->requests.end() ? nullptr : &found->second; +} + +bool RegisteredUser(std::int32_t userId) { + if (userId == SharedUser) return true; + std::int32_t list[20]; + for (auto& entry : list) entry = -1; + if (sceUserServiceGetRegisteredUserIdList(reinterpret_cast(list)) < 0) return false; + for (const auto entry : list) { + if (entry != -1 && entry == userId) return true; + } + return false; +} + +bool SystemUser(std::int32_t userId) { + return ((static_cast(userId) + 1) & ~0x100u) == 0; +} + +std::uint64_t Now() { + RtcTick tick{}; + sceRtcGetCurrentTick(&tick); + return tick.tick; +} + +std::uint64_t Resolution() { + return static_cast(sceRtcGetTickResolution()); +} + +bool EqualsIgnoreCase(std::string_view first, std::string_view second) { + if (first.size() != second.size()) return false; + for (std::size_t i = 0; i < first.size(); ++i) { + const auto lower = [](char value) { return value >= 'A' && value <= 'Z' ? static_cast(value - 'A' + 'a') : value; }; + if (lower(first[i]) != lower(second[i])) return false; + } + return true; +} + +std::string Lower(std::string_view text) { + std::string result(text); + for (auto& value : result) { + if (value >= 'A' && value <= 'Z') value = static_cast(value - 'A' + 'a'); + } + return result; +} + +Headers::const_iterator Find(const Headers& headers, std::string_view name) { + for (auto it = headers.begin(); it != headers.end(); ++it) { + if (EqualsIgnoreCase(it->name, name)) return it; + } + return headers.end(); +} + +bool Has(const Headers& headers, std::string_view name) { + return Find(headers, name) != headers.end(); +} + +std::string FirstValue(const Headers& headers, std::string_view name) { + const auto found = Find(headers, name); + return found == headers.end() ? std::string() : found->value; +} + +std::string JoinedValues(const Headers& headers, std::string_view name) { + std::string result; + bool first = true; + for (const auto& header : headers) { + if (!EqualsIgnoreCase(header.name, name)) continue; + if (!first) result += ','; + result += header.value; + first = false; + } + return result; +} + +void Append(Headers& headers, std::string_view name, std::string_view value) { + headers.push_back(Header{std::string(name), std::string(value)}); +} + +bool SetExisting(Headers& headers, std::string_view name, std::string_view value) { + for (auto& header : headers) { + if (EqualsIgnoreCase(header.name, name)) { + header.value = std::string(value); + return true; + } + } + return false; +} + +void SetOrAppend(Headers& headers, std::string_view name, std::string_view value) { + if (!SetExisting(headers, name, value)) Append(headers, name, value); +} + +void AppendUnlessSame(Headers& headers, std::string_view name, std::string_view value) { + const auto found = Find(headers, name); + if (found != headers.end() && EqualsIgnoreCase(found->value, value)) return; + Append(headers, name, value); +} + +void RemoveFirst(Headers& headers, std::string_view name) { + for (auto it = headers.begin(); it != headers.end(); ++it) { + if (EqualsIgnoreCase(it->name, name)) { + headers.erase(it); + return; + } + } +} + +std::vector Split(std::string_view text, char delimiter) { + std::vector parts; + std::size_t start = 0; + while (start < text.size()) { + std::size_t end = text.find(delimiter, start); + if (end == std::string_view::npos) end = text.size(); + parts.emplace_back(text.substr(start, end - start)); + start = end == text.size() ? end : end + 1; + } + return parts; +} + +std::string Trim(std::string_view text) { + constexpr std::string_view spaces = " \t\v\r\n"; + const auto first = text.find_first_not_of(spaces); + if (first == std::string_view::npos) return {}; + const auto last = text.find_last_not_of(spaces); + return std::string(text.substr(first, last - first + 1)); +} + +std::uint64_t ParseUnsigned(const std::string& text) { + errno = 0; + char* end = nullptr; + const unsigned long long value = std::strtoull(text.c_str(), &end, 10); + if (end == text.c_str() || errno == ERANGE) NotImplemented_nid_no_patch("sceHttpCache header number that the firmware's stoul rejects"); + return value; +} + +std::int64_t ParseSigned(const std::string& text) { + errno = 0; + char* end = nullptr; + const long long value = std::strtoll(text.c_str(), &end, 10); + if (end == text.c_str() || errno == ERANGE) NotImplemented_nid_no_patch("sceHttpCache header number that the firmware's stol rejects"); + return value; +} + +bool ParseDate(const std::string& text, std::uint64_t& tick) { + if (text.empty()) return false; + RtcTick parsed{}; + if (sceRtcParseDateTime(&parsed, text.c_str()) != 0) return false; + tick = parsed.tick; + return true; +} + +CacheControl ParseCacheControl(const Headers& headers) { + CacheControl control; + const auto tokens = Split(FirstValue(headers, "Cache-Control"), ','); + if (tokens.empty()) return control; + control.present = true; + const std::uint64_t resolution = Resolution(); + for (const auto& token : tokens) { + std::string unquoted; + for (const char value : token) { + if (value != '"') unquoted += value; + } + const auto parts = Split(Trim(unquoted), '='); + std::string name; + std::string value; + if (parts.size() == 1) { + name = parts[0]; + } else if (parts.size() == 2) { + name = parts[0]; + value = parts[1]; + } + if (!value.empty()) { + if (EqualsIgnoreCase(name, "max-age")) { + if (!control.maxAge) { + control.maxAgeTicks = ParseUnsigned(value) * resolution; + control.maxAge = true; + } + } else if (EqualsIgnoreCase(name, "s-maxage")) { + if (!control.sMaxage) { + control.sMaxageTicks = ParseUnsigned(value) * resolution; + control.sMaxage = true; + } + } else if (EqualsIgnoreCase(name, "max-stale")) { + if (!control.maxStale) { + control.maxStale = true; + const std::int64_t seconds = ParseSigned(value); + if (seconds < 0) { + control.maxStale = false; + } else { + control.maxStaleTicks = static_cast(seconds) * resolution; + control.maxStaleLimited = true; + } + } + } else if (EqualsIgnoreCase(name, "stale-while-revalidate")) { + if (!control.staleWhileRevalidate) { + control.staleWhileRevalidateTicks = ParseUnsigned(value) * resolution; + control.staleWhileRevalidate = true; + } + } else if (EqualsIgnoreCase(name, "stale-if-error")) { + if (!control.staleIfError) { + control.staleIfErrorTicks = ParseUnsigned(value) * resolution; + control.staleIfError = true; + } + } + } else if (EqualsIgnoreCase(name, "no-cache")) { + control.noCache = true; + } else if (EqualsIgnoreCase(name, "no-store")) { + control.noStore = true; + } else if (EqualsIgnoreCase(name, "must-revalidate")) { + control.mustRevalidate = true; + } else if (EqualsIgnoreCase(name, "proxy-revalidate")) { + control.proxyRevalidate = true; + } else if (EqualsIgnoreCase(name, "private")) { + control.isPrivate = true; + } else if (EqualsIgnoreCase(name, "public")) { + control.isPublic = true; + } else if (EqualsIgnoreCase(name, "max-stale")) { + control.maxStale = true; + } + } + return control; +} + +bool StorableHeaders(const Headers& headers) { + const CacheControl control = ParseCacheControl(headers); + if (control.present && control.noStore) return false; + if (Has(headers, "Expires") || Has(headers, "Last-Modified")) return true; + return control.present && (control.maxAge || control.sMaxage); +} + +bool StorableStatus(std::uint32_t status) { + return status == 200 || status == 301 || status == 308; +} + +bool ServerError(std::uint32_t status) { + return status == 500 || status == 502 || status == 503 || status == 504; +} + +std::uint64_t Lifetime(const ResponseData& response) { + const CacheControl control = ParseCacheControl(response.headers); + if (control.present && control.maxAge) return control.maxAgeTicks; + if (control.present && control.sMaxage) return control.sMaxageTicks; + std::uint64_t date = response.time; + ParseDate(FirstValue(response.headers, "Date"), date); + std::uint64_t expires = 0; + if (ParseDate(FirstValue(response.headers, "Expires"), expires)) return expires - date; + std::uint64_t lastModified = 0; + if (ParseDate(FirstValue(response.headers, "Last-Modified"), lastModified)) return (date - lastModified) / 10; + return 0; +} + +bool Heuristic(const Headers& headers) { + const CacheControl control = ParseCacheControl(headers); + if (control.present && (control.sMaxage || control.maxAge)) return false; + return !Has(headers, "Expires"); +} + +std::uint64_t CurrentAge(const Entry& entry) { + const std::uint64_t now = Now(); + std::uint64_t date = 0; + std::uint64_t apparentAge = 0; + if (ParseDate(FirstValue(entry.headers, "Date"), date)) apparentAge = entry.responseTime > date ? entry.responseTime - date : 0; + const std::string ageText = FirstValue(entry.headers, "Age"); + const std::uint64_t ageValue = ageText.empty() ? 0 : ParseUnsigned(ageText) * Resolution(); + const std::uint64_t correctedAge = apparentAge > ageValue ? apparentAge : ageValue; + const std::uint64_t residentTime = now > entry.responseTime ? now - entry.responseTime : 0; + return correctedAge + residentTime; +} + +bool VaryMatches(const Headers& vary, const Headers& requestHeaders) { + for (const auto& header : vary) { + if (FirstValue(requestHeaders, header.name) != header.value) return false; + } + return true; +} + +Headers SelectVary(const Headers& requestHeaders, const Headers& responseHeaders) { + Headers vary; + const std::string names = JoinedValues(responseHeaders, "Vary"); + if (names.empty()) return vary; + for (const auto& name : Split(names, ',')) { + const std::string trimmed = Trim(name); + Append(vary, trimmed, FirstValue(requestHeaders, trimmed)); + } + return vary; +} + +bool AddValidators(const Headers& entryHeaders, Headers& headers) { + const std::string lastModified = FirstValue(entryHeaders, "Last-Modified"); + const std::string etag = FirstValue(entryHeaders, "ETag"); + if (!lastModified.empty()) Append(headers, "If-Modified-Since", lastModified); + if (!etag.empty()) Append(headers, "If-None-Match", etag); + return !lastModified.empty() || !etag.empty(); +} + +bool StaleUsable(const Entry* entry) { + if (entry == nullptr) return false; + const CacheControl control = ParseCacheControl(entry->headers); + if (!Has(entry->headers, "Age") || !control.staleWhileRevalidate) return false; + for (const auto& header : entry->headers) { + if (EqualsIgnoreCase(header.name, "Warning") && EqualsIgnoreCase(header.value, StaleWarning)) return true; + } + return false; +} + +struct Decision { + int result = ResultMiss; + bool remove = false; + Headers next; +}; + +Decision Validate(const Headers& requestHeaders, Entry& entry) { + Decision decision; + if (!entry.vary.empty() && !VaryMatches(entry.vary, requestHeaders)) return decision; + for (const auto name : ConditionalHeaders) { + if (Has(requestHeaders, name)) return decision; + } + const auto revalidate = [&] { + decision.next = requestHeaders; + if (AddValidators(entry.headers, decision.next)) { + decision.result = ResultRevalidate; + } else { + decision.remove = true; + } + return decision; + }; + const CacheControl requestControl = ParseCacheControl(requestHeaders); + if (requestControl.present && requestControl.noCache) return revalidate(); + if (EqualsIgnoreCase(FirstValue(requestHeaders, "Pragma"), "no-cache")) return revalidate(); + const CacheControl responseControl = ParseCacheControl(entry.headers); + if (responseControl.present && (responseControl.noCache || responseControl.mustRevalidate || responseControl.proxyRevalidate)) return revalidate(); + const std::uint64_t lifetime = entry.lifetime; + const std::uint64_t age = CurrentAge(entry); + if (Heuristic(entry.headers) && age >= HeuristicWarningSeconds * Resolution()) Append(entry.headers, "Warning", HeuristicWarning); + if (age < lifetime) { + decision.result = ResultHit; + return decision; + } + if (requestControl.maxStale && (!requestControl.maxStaleLimited || age - lifetime < requestControl.maxStaleTicks)) { + Append(entry.headers, "Warning", StaleWarning); + decision.result = ResultHit; + return decision; + } + revalidate(); + if (decision.result == ResultRevalidate && responseControl.staleWhileRevalidate && responseControl.staleWhileRevalidateTicks + lifetime > age) { + AppendUnlessSame(entry.headers, "Warning", StaleWarning); + SetOrAppend(entry.headers, "Age", std::to_string(age / Resolution())); + } + return decision; +} + +int ConvertHeaders(std::size_t count, const HttpCacheHeader* source, Headers& headers) { + if (source == nullptr) return count != 0 ? ErrorInvalidValue : 0; + for (std::size_t i = 0; i < count; ++i) { + if (source[i].name == nullptr || source[i].value == nullptr) return ErrorInvalidValue; + Append(headers, source[i].name, source[i].value); + } + return 0; +} + +StoreKey KeyFor(const Request& request) { + return StoreKey{request.storeUser, request.current->url, FirstValue(request.current->headers, "User-Agent")}; +} + +void RejectSharedEntry(const Request& request) { + if (request.storeUser == SharedUser) return; + const auto& key = KeyFor(request); + if (Store.count(StoreKey{SharedUser, std::get<1>(key), std::get<2>(key)}) != 0) { + NotImplemented_nid_no_patch("sceHttpCache lookup by a user of an entry kept for all users"); + } +} + +void ApplySharing(Request& request, Entry& entry) { + entry.isPublic = CacheSharing != 0 && ParseCacheControl(entry.headers).isPublic; + if (!entry.isPublic) return; + if (request.storeUser != SharedUser) NotImplemented_nid_no_patch("sceHttpCache storing a public response for all users"); +} + +void RemoveStored(const Request& request) { + if (request.hasStore && request.current) Store.erase(KeyFor(request)); +} + +std::shared_ptr LookupStored(const Request& request) { + RejectSharedEntry(request); + const auto found = Store.find(KeyFor(request)); + if (found == Store.end()) return nullptr; + auto entry = std::make_shared(found->second); + entry->pending = false; + entry->readOffset = 0; + entry->key = found->first; + return entry; +} + +void FillEntry(Request& request, Entry& entry, const ResponseData& response) { + entry.status = response.status; + entry.headers = response.headers; + entry.responseTime = response.time; + entry.lifetime = Lifetime(response); + entry.vary = SelectVary(request.current->headers, response.headers); + ApplySharing(request, entry); +} + +const HttpCacheHeader* ExportHeaders(Request& request, const Headers& headers) { + if (headers.empty()) NotImplemented_nid_no_patch("sceHttpCache returning an empty header list"); + auto& array = request.headerArrays.emplace_back(); + for (const auto& header : headers) { + const char* name = request.strings.emplace_back(header.name).c_str(); + const char* value = request.strings.emplace_back(header.value).c_str(); + array.push_back(HttpCacheHeader{name, value}); + } + return array.data(); +} + +void FillResponse(Request& request, const Entry& entry, HttpCacheResponse* response) { + response->statusCode = entry.status; + response->headerCount = entry.headers.size(); + response->headers = ExportHeaders(request, entry.headers); + response->contentLength = entry.pending ? 0 : entry.body.size(); +} + +void FillNextRequest(Request& request, const RequestData& next, HttpCacheRequest* nextRequest) { + nextRequest->method = next.method; + nextRequest->headerCount = next.headers.size(); + nextRequest->headers = ExportHeaders(request, next.headers); + nextRequest->url = request.strings.emplace_back(next.url).c_str(); +} + +int RetrieveFor(Request& request, RequestData data, std::shared_ptr& found, const RequestData*& next) { + found = nullptr; + next = nullptr; + if (request.entry && request.current && request.current->url == data.url) { + found = request.entry; + if (request.lastResult == ResultRevalidate) next = request.next ? &*request.next : nullptr; + return request.lastResult; + } + request.state = State::Idle; + if (data.method != 0) return ResultMiss; + request.current = std::move(data); + if (request.hasStore) request.entry = LookupStored(request); + if (!request.entry) { + request.state = State::Miss; + request.lastResult = ResultMiss; + return ResultMiss; + } + Decision decision = Validate(request.current->headers, *request.entry); + if (decision.result == ResultHit) { + request.state = State::Hit; + found = request.entry; + } else if (decision.result == ResultRevalidate) { + request.next = RequestData{request.current->method, request.current->url, std::move(decision.next)}; + next = &*request.next; + if (StaleUsable(request.entry.get())) { + found = request.entry; + request.state = State::RevalidateWithStale; + } else { + request.state = State::Revalidate; + } + } else { + if (decision.remove) RemoveStored(request); + request.state = State::Miss; + } + request.lastResult = decision.result; + return decision.result; +} + +bool HeadersUnchanged(const Headers& stored, const Headers& received) { + if (stored.size() != received.size()) return false; + for (const auto& header : received) { + if (!EqualsIgnoreCase(FirstValue(stored, header.name), header.value)) return false; + } + return true; +} + +bool MergedHeader(std::string_view name) { + const std::string lower = Lower(name); + for (const auto excluded : UnmergedHeaders) { + if (lower == excluded) return false; + } + for (const auto excluded : UnmergedHeaderNames) { + if (lower == excluded) return false; + } + return true; +} + +bool Revalidated(const ResponseData& response, Entry& entry) { + if (ServerError(response.status)) { + const CacheControl control = ParseCacheControl(entry.headers); + if (!control.present || !control.staleIfError) return false; + const std::uint64_t age = CurrentAge(entry); + if (control.staleIfErrorTicks + entry.lifetime <= age) return false; + AppendUnlessSame(entry.headers, "Warning", StaleWarning); + SetOrAppend(entry.headers, "Age", std::to_string(age / Resolution())); + return true; + } + if (response.status != 304) return false; + Headers merged = entry.headers; + for (const auto& header : response.headers) { + if (MergedHeader(header.name)) SetExisting(merged, header.name, header.value); + } + RemoveFirst(merged, "Warning"); + RemoveFirst(merged, "Age"); + entry.headers = std::move(merged); + return true; +} + +int Revalidate(Request& request, const ResponseData& response, std::shared_ptr& found) { + found = nullptr; + if (request.state < State::Revalidate || !request.entry) return ResultMiss; + const bool serverError = ServerError(response.status); + if (!serverError && !StorableHeaders(response.headers)) { + RemoveStored(request); + return ResultMiss; + } + Entry& entry = *request.entry; + const bool unchanged = HeadersUnchanged(entry.headers, response.headers); + int result = ResultMiss; + if (Revalidated(response, entry)) { + if (!serverError && !unchanged) { + entry.responseTime = Now(); + entry.lifetime = Lifetime(response); + ApplySharing(request, entry); + const auto stored = Store.find(entry.key); + if (!entry.pending && stored != Store.end()) { + stored->second.status = entry.status; + stored->second.headers = entry.headers; + stored->second.responseTime = entry.responseTime; + stored->second.lifetime = entry.lifetime; + stored->second.isPublic = entry.isPublic; + } + } + result = ResultHit; + request.state = State::Validated; + found = request.entry; + } else if (StorableStatus(response.status)) { + FillEntry(request, entry, response); + entry.body.clear(); + entry.pending = true; + entry.key = KeyFor(request); + request.state = State::Replacing; + } else { + RemoveStored(request); + request.state = State::Dropped; + } + request.lastResult = result; + return result; +} + +int MakeResponse(const HttpCacheResponseHeader* header, ResponseData& response) { + response.status = header->statusCode; + if (const int status = ConvertHeaders(header->headerCount, header->headers, response.headers); status != 0) return status; + response.time = Now(); + return 0; +} + +} + extern "C" { int APS5_VABI sceHttpCacheClear(std::int32_t cacheId, std::int32_t userId, const char* tag) { @@ -23,18 +731,47 @@ int APS5_VABI sceHttpCacheClear(std::int32_t cacheId, std::int32_t userId, const } int APS5_VABI sceHttpCacheClearAll(std::int32_t cacheId, std::int32_t userId) { - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(Mutex); + if (FindCache(cacheId) == nullptr) return ErrorNotFound; + if (SystemUser(userId) || !RegisteredUser(userId)) return 0; + for (const auto& stored : Store) { + if (std::get<0>(stored.first) == SharedUser && userId != SharedUser) { + NotImplemented_nid_no_patch("sceHttpCacheClearAll for a user while entries kept for all users exist"); + } + } + for (auto it = Store.begin(); it != Store.end();) { + it = std::get<0>(it->first) == userId ? Store.erase(it) : std::next(it); + } return 0; } int APS5_VABI sceHttpCacheCompleteRequest(std::int32_t requestId) { - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(Mutex); + Request* request = FindRequest(requestId); + if (request == nullptr) return ErrorNotFound; + if (request->hasStore && request->entry && request->entry->pending) { + Entry stored = *request->entry; + stored.pending = false; + stored.readOffset = 0; + Store[stored.key] = std::move(stored); + request->entry->pending = false; + } return 0; } int APS5_VABI sceHttpCacheCreateRequest(std::int32_t cacheId, std::int32_t userId) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(Mutex); + Cache* cache = FindCache(cacheId); + if (cache == nullptr) return ErrorNotFound; + if (!SystemUser(userId) && !RegisteredUser(userId)) return ErrorInvalidUser; + const std::uint32_t sequence = cache->nextRequest; + cache->nextRequest = sequence + 1 < RequestCounterLimit ? sequence + 1 : 1; + const auto requestId = static_cast(static_cast(cacheId) << 24 | sequence); + Request request; + request.storeUser = userId; + request.hasStore = !SystemUser(userId); + cache->requests[requestId] = std::move(request); + return requestId; } int APS5_VABI sceHttpCacheCreateRequestWithTag(std::int32_t cacheId, std::int32_t userId, const char* tag) { @@ -43,22 +780,75 @@ int APS5_VABI sceHttpCacheCreateRequestWithTag(std::int32_t cacheId, std::int32_ } int APS5_VABI sceHttpCacheDeleteRequest(std::int32_t requestId) { - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(Mutex); + Cache* cache = RequestCache(requestId); + if (cache == nullptr) return ErrorNotFound; + cache->requests.erase(requestId); return 0; } int APS5_VABI sceHttpCacheInit(std::size_t poolSize) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(Mutex); + if (!UserServiceStarted) { + const int status = sceUserServiceInitialize(nullptr); + if (status < 0 && status != UserServiceAlreadyInitialized) { + if (status == UserServiceNoMemory) return ErrorNoMemory; + if (status == UserServiceInvalidArgument) return ErrorInvalidValue; + return ErrorUnknown; + } + UserServiceStarted = true; + } + if (Caches.size() >= MaxCaches) return ErrorTooManyCaches; + (void)poolSize; + std::int32_t cacheId = LastCacheId; + do { + cacheId = cacheId % static_cast(MaxCaches) + 1; + } while (Caches.count(cacheId) != 0); + LastCacheId = cacheId; + Caches[cacheId] = Cache{}; + return cacheId; } int APS5_VABI sceHttpCacheReadData(std::int32_t requestId, std::size_t size, void* data) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (data == nullptr && size != 0) return ErrorInvalidValue; + std::lock_guard lock(Mutex); + Request* request = FindRequest(requestId); + if (request == nullptr) return ErrorNotFound; + if (request->state != State::Hit && request->state != State::RevalidateWithStale && request->state != State::Validated) return 0; + Entry& entry = *request->entry; + if (entry.pending) return 0; + const std::size_t remaining = entry.body.size() - entry.readOffset; + const std::size_t count = size < remaining ? size : remaining; + if (count != 0) std::memcpy(data, entry.body.data() + entry.readOffset, count); + entry.readOffset += count; + return static_cast(count); } int APS5_VABI sceHttpCacheRetrieve(std::int32_t requestId, const HttpCacheRequest* request, HttpCacheResponse* response, HttpCacheRequest* nextRequest, std::int32_t* result) { - NotImplemented_nid_no_patch(__func__); + if (request == nullptr || result == nullptr) return ErrorInvalidValue; + std::lock_guard lock(Mutex); + Request* state = FindRequest(requestId); + if (state == nullptr) return ErrorNotFound; + RequestData data; + if (const int status = ConvertHeaders(request->headerCount, request->headers, data.headers); status != 0) return status; + if (request->url == nullptr) NotImplemented_nid_no_patch("sceHttpCacheRetrieve with a null URL"); + data.method = request->method; + data.url = request->url; + std::shared_ptr found; + const RequestData* next = nullptr; + *result = RetrieveFor(*state, std::move(data), found, next); + if (*result == ResultHit) { + if (response == nullptr) return ErrorInvalidValue; + FillResponse(*state, *found, response); + return 0; + } + if (*result != ResultRevalidate) return 0; + if (nextRequest == nullptr) return ErrorInvalidValue; + if (next == nullptr) NotImplemented_nid_no_patch("sceHttpCacheRetrieve revalidation without a next request"); + FillNextRequest(*state, *next, nextRequest); + if (!StaleUsable(found.get())) return 0; + if (response == nullptr) return ErrorInvalidValue; + FillResponse(*state, *found, response); return 0; } @@ -68,7 +858,17 @@ int APS5_VABI sceHttpCacheRetrieveWithMemoryPool(std::int32_t requestId, const H } int APS5_VABI sceHttpCacheRevalidate(std::int32_t requestId, const HttpCacheResponseHeader* header, HttpCacheResponse* response, std::int32_t* result) { - NotImplemented_nid_no_patch(__func__); + if (header == nullptr || result == nullptr) return ErrorInvalidValue; + std::lock_guard lock(Mutex); + Request* state = FindRequest(requestId); + if (state == nullptr) return ErrorNotFound; + ResponseData received; + if (const int status = MakeResponse(header, received); status != 0) return status; + std::shared_ptr found; + *result = Revalidate(*state, received, found); + if (!found) return 0; + if (response == nullptr) return ErrorInvalidValue; + FillResponse(*state, *found, response); return 0; } @@ -78,22 +878,55 @@ int APS5_VABI sceHttpCacheRevalidateWithMemoryPool(std::int32_t requestId, const } int APS5_VABI sceHttpCacheSetCacheSharing(std::uint8_t enabled) { - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(Mutex); + CacheSharing = enabled; return 0; } int APS5_VABI sceHttpCacheSetData(std::int32_t requestId, const void* data, std::size_t size) { - NotImplemented_nid_no_patch(__func__); + if (data == nullptr) return ErrorInvalidValue; + std::lock_guard lock(Mutex); + Request* request = FindRequest(requestId); + if (request == nullptr) return ErrorNotFound; + if ((request->state == State::Miss || request->state == State::Replacing) && request->entry && request->entry->pending) { + request->entry->body.append(static_cast(data), size); + } return 0; } int APS5_VABI sceHttpCacheSetQuota(std::int32_t cacheId, const HttpCacheQuota* quota) { + { + std::lock_guard lock(Mutex); + if (FindCache(cacheId) == nullptr) return ErrorNotFound; + } + if (quota == nullptr) return ErrorInvalidValue; + if (quota->count != 0) { + if (quota->entries == nullptr) return ErrorInvalidValue; + for (std::size_t i = 0; i < quota->count; ++i) { + if (quota->entries[i].name == nullptr || quota->entries[i].name[0] == '\0') return ErrorInvalidValue; + } + } NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceHttpCacheSetResponseHeader(std::int32_t requestId, const HttpCacheResponseHeader* header) { - NotImplemented_nid_no_patch(__func__); + if (header == nullptr) return ErrorInvalidValue; + std::lock_guard lock(Mutex); + Request* request = FindRequest(requestId); + if (request == nullptr) return ErrorNotFound; + ResponseData received; + if (const int status = MakeResponse(header, received); status != 0) return status; + if (request->state == State::Idle || !request->hasStore || !request->current) return 0; + if (!StorableStatus(received.status) || !StorableHeaders(received.headers)) { + RemoveStored(*request); + return 0; + } + auto entry = std::make_shared(); + FillEntry(*request, *entry, received); + entry->pending = true; + entry->key = KeyFor(*request); + request->entry = std::move(entry); return 0; } @@ -121,11 +954,22 @@ int APS5_VABI sceHttpCacheSystemTerm() { } int APS5_VABI sceHttpCacheTerm(std::int32_t cacheId) { - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(Mutex); + if (Caches.erase(cacheId) == 0) return ErrorNotFound; + if (Caches.empty() && UserServiceStarted) { + sceUserServiceTerminate(); + UserServiceStarted = false; + } return 0; } -int APS5_VABI sceHttpCacheClearAllUserTags() { + +int APS5_VABI sceHttpCacheClearAllUserTags(std::int32_t cacheId, const std::int32_t* userIds, std::size_t count) { + { + std::lock_guard lock(Mutex); + if (FindCache(cacheId) == nullptr) return ErrorNotFound; + } + if (count > 20 || (userIds == nullptr && count != 0)) return ErrorInvalidValue; NotImplemented_nid_no_patch(__func__); return 0; } @@ -136,11 +980,11 @@ int APS5_VABI sceHttpCacheClearCacheInfo() { } int APS5_VABI sceHttpCacheDebugSetUrl() { - NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpCacheRegisterCacheInfoUrl() { +int APS5_VABI sceHttpCacheRegisterCacheInfoUrl(const void* info) { + if (info == nullptr) return ErrorInvalidValue; NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceHttpCache/include/Api.hpp b/core/libs/prx/libSceHttpCache/include/Api.hpp index 634bf42348..508651200b 100644 --- a/core/libs/prx/libSceHttpCache/include/Api.hpp +++ b/core/libs/prx/libSceHttpCache/include/Api.hpp @@ -66,10 +66,10 @@ int APS5_VABI sceHttpCacheSystemSendStatistics(); int APS5_VABI sceHttpCacheSystemShutdown(); int APS5_VABI sceHttpCacheSystemTerm(); int APS5_VABI sceHttpCacheTerm(std::int32_t cacheId); -int APS5_VABI sceHttpCacheClearAllUserTags(); +int APS5_VABI sceHttpCacheClearAllUserTags(std::int32_t cacheId, const std::int32_t* userIds, std::size_t count); int APS5_VABI sceHttpCacheClearCacheInfo(); int APS5_VABI sceHttpCacheDebugSetUrl(); -int APS5_VABI sceHttpCacheRegisterCacheInfoUrl(); +int APS5_VABI sceHttpCacheRegisterCacheInfoUrl(const void* info); int APS5_VABI sceHttpCacheRemoveTag(); } diff --git a/core/libs/prx/libSceIcu/CMakeLists.txt b/core/libs/prx/libSceIcu/CMakeLists.txt new file mode 100644 index 0000000000..5fcc2e7744 --- /dev/null +++ b/core/libs/prx/libSceIcu/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceIcu SHARED EXCLUDE_FROM_ALL Export.cpp Invariant.cpp Version.cpp) +target_include_directories(libSceIcu PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceIcu) + +if(BUILD_TESTING) + add_test_executable(guest_icu_version_tests ${LIBS_INCLUDE_DIR}/tests/GuestIcuVersion.cpp) + target_include_directories(guest_icu_version_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_icu_version_tests PRIVATE libSceIcu libc) + 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) + if(WIN32) + set_tests_properties(guest_icu_version PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceIcu/Invariant.cpp b/core/libs/prx/libSceIcu/Invariant.cpp new file mode 100644 index 0000000000..53501ba1d6 --- /dev/null +++ b/core/libs/prx/libSceIcu/Invariant.cpp @@ -0,0 +1,31 @@ +#include "prx/libSceIcu/libSceIcu.hpp" +#include "prx/libc/include/General.hpp" +#include +#include + +namespace { + +constexpr std::array InvariantChars{0xfffffbff, 0xffffffe5, 0x87fffffe, 0x87fffffe}; + +bool Invariant(char16_t unit) { + return unit <= 0x7f && (InvariantChars[unit >> 5] & (1u << (unit & 0x1f))) != 0; +} + +} + +extern "C" { + +void APS5_VABI u_charsToUChars_nid_postfix(const char* chars, char16_t* units, std::int32_t length) { + if (length > 0 && (!chars || !units)) APS5_INVALID_ARG_EX; + for (std::int32_t index = 0; index < length; ++index) units[index] = static_cast(chars[index]); +} + +void APS5_VABI u_UCharsToChars_nid_postfix(const char16_t* units, char* chars, std::int32_t length) { + if (length > 0 && (!units || !chars)) APS5_INVALID_ARG_EX; + for (std::int32_t index = 0; index < length; ++index) { + const char16_t unit = units[index]; + chars[index] = Invariant(unit) ? static_cast(unit) : 0; + } +} + +} diff --git a/core/libs/prx/libSceIcu/Invariant.hpp b/core/libs/prx/libSceIcu/Invariant.hpp new file mode 100644 index 0000000000..77e2421ef2 --- /dev/null +++ b/core/libs/prx/libSceIcu/Invariant.hpp @@ -0,0 +1,8 @@ +#pragma once +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +extern "C" { +void APS5_VABI u_charsToUChars_nid_postfix(const char*, char16_t*, std::int32_t); +void APS5_VABI u_UCharsToChars_nid_postfix(const char16_t*, char*, std::int32_t); +} diff --git a/core/libs/prx/libSceIcu/Version.cpp b/core/libs/prx/libSceIcu/Version.cpp new file mode 100644 index 0000000000..5916903a82 --- /dev/null +++ b/core/libs/prx/libSceIcu/Version.cpp @@ -0,0 +1,69 @@ +#include "prx/libSceIcu/libSceIcu.hpp" +#include "prx/libc/include/Api.hpp" +#include + +namespace { + +constexpr std::uint16_t VersionLength = 4; +constexpr std::int32_t VersionStringLength = 20; +constexpr char VersionDelimiter = '.'; + +char* WriteField(char* text, std::uint8_t field) { + if (field >= 100) { + *text++ = static_cast('0' + field / 100); + field %= 100; + } + if (field >= 10) { + *text++ = static_cast('0' + field / 10); + field %= 10; + } + *text++ = static_cast('0' + field); + return text; +} + +} + +extern "C" { + +void APS5_VABI u_versionFromString_nid_postfix(std::uint8_t* version, const char* text) { + if (!version) return; + std::uint16_t part = 0; + if (text) { + for (;;) { + char* end = nullptr; + version[part] = static_cast(strtoul_nid_postfix(text, &end, 10)); + if (end == text || ++part == VersionLength || *end != VersionDelimiter) break; + text = end + 1; + } + } + while (part < VersionLength) version[part++] = 0; +} + +void APS5_VABI u_versionFromUString_nid_postfix(std::uint8_t* version, const char16_t* text) { + if (!version || !text) return; + char chars[VersionStringLength + 1]; + std::int32_t length = 0; + while (length < VersionStringLength && text[length]) ++length; + u_UCharsToChars_nid_postfix(text, chars, length); + chars[length] = 0; + u_versionFromString_nid_postfix(version, chars); +} + +void APS5_VABI u_versionToString_nid_postfix(const std::uint8_t* version, char* text) { + if (!text) return; + if (!version) { + text[0] = 0; + return; + } + std::uint16_t count = VersionLength; + while (count > 0 && version[count - 1] == 0) --count; + if (count <= 1) count = 2; + text = WriteField(text, version[0]); + for (std::uint16_t part = 1; part < count; ++part) { + *text++ = VersionDelimiter; + text = WriteField(text, version[part]); + } + *text = 0; +} + +} diff --git a/core/libs/prx/libSceIcu/Version.hpp b/core/libs/prx/libSceIcu/Version.hpp new file mode 100644 index 0000000000..4963a77ed2 --- /dev/null +++ b/core/libs/prx/libSceIcu/Version.hpp @@ -0,0 +1,9 @@ +#pragma once +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +extern "C" { +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*); +} diff --git a/core/libs/prx/libSceIcu/libSceIcu.hpp b/core/libs/prx/libSceIcu/libSceIcu.hpp index 6f70f09bee..7b35c6d5fb 100644 --- a/core/libs/prx/libSceIcu/libSceIcu.hpp +++ b/core/libs/prx/libSceIcu/libSceIcu.hpp @@ -1 +1,3 @@ #pragma once +#include "Invariant.hpp" +#include "Version.hpp" diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index a6c3a3d181..ff3fdeedde 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -462,7 +462,7 @@ int APS5_VABI sceImeKeyboardGetResourceId(int32_t user_id, KeyboardResourceIdArr } int APS5_VABI sceImeKeyboardOpen(int32_t user_id, const KeyboardParam* param) { - if (!param) APS5_INVALID_ARG_EX; + if (!param) return ErrorInvalidAddress; std::lock_guard lock(g_keyboardMutex); if (!g_openKeyboards.insert(user_id).second) return ErrorBusy; return 0; @@ -530,7 +530,7 @@ int APS5_VABI sceImeSetTextGeometry(TextAreaMode mode, const TextGeometry* geome } int APS5_VABI sceImeUpdate(EventHandler handler) { - if (!handler) APS5_INVALID_ARG_EX; + if (!handler) return ErrorInvalidHandler; for (;;) { ImeEvent event{}; void* arg = nullptr; diff --git a/core/libs/prx/libSceImeBackend/Export.cpp b/core/libs/prx/libSceImeBackend/Export.cpp index 28d44ccb82..081f2a8da3 100644 --- a/core/libs/prx/libSceImeBackend/Export.cpp +++ b/core/libs/prx/libSceImeBackend/Export.cpp @@ -1,2 +1,117 @@ +#include "prx/libSceImeBackend/include/Api.hpp" #include "prx/libSceIme/Export.cpp" #include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceImeBackendParamInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeBackendOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeBackendClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeBackendReset(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeBackendGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeBackendEnterCharacter(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeBackendBackspaceCharacter(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeBackendPartConfirm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeBackendAllConfirm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeBackendAllDeleteConvertString(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeBackendConvertForward(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeBackendConvertBackward(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeBackendConvertCancel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeBackendGetString(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeBackendGetConvertString(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeBackendGetConfirmString(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeBackendGetCaretIndex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeBackendModeCaretNext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeBackendModeCaretPrevious(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeBackendGetCandidateList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeBackendGetCandidateListSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeBackendGetCandidateSelect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceImeBackend/include/Api.hpp b/core/libs/prx/libSceImeBackend/include/Api.hpp new file mode 100644 index 0000000000..313043e3c0 --- /dev/null +++ b/core/libs/prx/libSceImeBackend/include/Api.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceImeBackendParamInit(void); +int APS5_VABI sceImeBackendOpen(void); +int APS5_VABI sceImeBackendClose(void); +int APS5_VABI sceImeBackendReset(void); +int APS5_VABI sceImeBackendGetStatus(void); +int APS5_VABI sceImeBackendEnterCharacter(void); +int APS5_VABI sceImeBackendBackspaceCharacter(void); +int APS5_VABI sceImeBackendPartConfirm(void); +int APS5_VABI sceImeBackendAllConfirm(void); +int APS5_VABI sceImeBackendAllDeleteConvertString(void); +int APS5_VABI sceImeBackendConvertForward(void); +int APS5_VABI sceImeBackendConvertBackward(void); +int APS5_VABI sceImeBackendConvertCancel(void); +int APS5_VABI sceImeBackendGetString(void); +int APS5_VABI sceImeBackendGetConvertString(void); +int APS5_VABI sceImeBackendGetConfirmString(void); +int APS5_VABI sceImeBackendGetCaretIndex(void); +int APS5_VABI sceImeBackendModeCaretNext(void); +int APS5_VABI sceImeBackendModeCaretPrevious(void); +int APS5_VABI sceImeBackendGetCandidateList(void); +int APS5_VABI sceImeBackendGetCandidateListSize(void); +int APS5_VABI sceImeBackendGetCandidateSelect(void); + +} diff --git a/core/libs/prx/libSceImeBackend/libSceImeBackend.hpp b/core/libs/prx/libSceImeBackend/libSceImeBackend.hpp index 6f70f09bee..242c789fdb 100644 --- a/core/libs/prx/libSceImeBackend/libSceImeBackend.hpp +++ b/core/libs/prx/libSceImeBackend/libSceImeBackend.hpp @@ -1 +1,3 @@ #pragma once + +#include "prx/libSceImeBackend/include/Api.hpp" diff --git a/core/libs/prx/libSceJobManager/CMakeLists.txt b/core/libs/prx/libSceJobManager/CMakeLists.txt index 4a88c6da6d..1364b0e64d 100644 --- a/core/libs/prx/libSceJobManager/CMakeLists.txt +++ b/core/libs/prx/libSceJobManager/CMakeLists.txt @@ -1,9 +1,24 @@ add_library(libSceJobManager SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceJobManager PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(libSceJobManager PRIVATE libSceAgc libSceAgcDriver) +target_link_libraries(libSceJobManager PRIVATE libSceAgc libSceAgcDriver libSceRazorCpu ulobjmgr) configure_windows_unwind(libSceJobManager) if(BUILD_TESTING) + add_test_executable(guest_job_manager_profiler_tests ${LIBS_INCLUDE_DIR}/tests/JobManagerProfiler.cpp Export.cpp) + target_include_directories(guest_job_manager_profiler_tests PRIVATE ${LIBS_INCLUDE_DIR} + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR} + $) + target_compile_options(guest_job_manager_profiler_tests PRIVATE -msse4.2) + target_link_libraries(guest_job_manager_profiler_tests PRIVATE libSceAgc libSceAgcDriver libkernel libc ulobjmgr) + configure_windows_unwind(guest_job_manager_profiler_tests) + add_test(NAME guest_job_manager_profiler COMMAND guest_job_manager_profiler_tests) + add_test(NAME guest_job_manager_profiler_gpu COMMAND guest_job_manager_profiler_tests gpu) + set_tests_properties(guest_job_manager_profiler guest_job_manager_profiler_gpu PROPERTIES TIMEOUT 20) + set_tests_properties(guest_job_manager_profiler_gpu PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(guest_job_manager_profiler guest_job_manager_profiler_gpu PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(job_manager_compute_progress_tests ${LIBS_INCLUDE_DIR}/tests/JobManagerComputeProgress.cpp) target_include_directories(job_manager_compute_progress_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(job_manager_compute_progress_tests PRIVATE libc) @@ -14,22 +29,43 @@ if(BUILD_TESTING) set_tests_properties(guest_job_manager_compute_progress PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_job_manager_failure_tests ${LIBS_INCLUDE_DIR}/tests/JobManagerFailures.cpp Export.cpp) + target_include_directories(guest_job_manager_failure_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_job_manager_failure_tests PRIVATE libSceAgc libSceAgcDriver libSceRazorCpu libkernel libc ulobjmgr) + configure_windows_unwind(guest_job_manager_failure_tests) + add_test(NAME guest_job_manager_failures COMMAND guest_job_manager_failure_tests) + set_tests_properties(guest_job_manager_failures PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_job_manager_failures PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_job_manager_registration_tests ${LIBS_INCLUDE_DIR}/tests/JobManagerRegistration.cpp Export.cpp) + target_include_directories(guest_job_manager_registration_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_job_manager_registration_tests PRIVATE libSceAgc libSceAgcDriver libSceRazorCpu libkernel libc) + configure_windows_unwind(guest_job_manager_registration_tests) + add_test(NAME guest_job_manager_registration COMMAND guest_job_manager_registration_tests) + set_tests_properties(guest_job_manager_registration PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_job_manager_registration PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_job_manager_tests ${LIBS_INCLUDE_DIR}/tests/GuestJobManager.cpp) target_include_directories(guest_job_manager_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_job_manager_tests PRIVATE libSceJobManager libSceAgc libSceAgcDriver libkernel libc) configure_windows_unwind(guest_job_manager_tests) - if(WIN32) + if(WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux") add_library(job_manager_reference SHARED EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestJobManager.cpp ${LIBS_INCLUDE_DIR}/tests/JobManagerReference.cpp) target_include_directories(job_manager_reference PRIVATE ${LIBS_INCLUDE_DIR}) target_compile_definitions(job_manager_reference PRIVATE ANYPS5_JOB_MANAGER_REFERENCE) - target_link_libraries(job_manager_reference PRIVATE libkernel libc) + target_link_libraries(job_manager_reference PRIVATE libkernel libc ${CMAKE_DL_LIBS}) set_target_properties(job_manager_reference PROPERTIES PREFIX "" SUFFIX .prx - RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/tests) + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/tests + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/tests) configure_windows_unwind(job_manager_reference) endif() - foreach(testCase IN ITEMS memory validation dispatch ordering nested limits node_pool priority shutdown worker_policy worker_wake worker_wait worker_count worker_spin) + foreach(testCase IN ITEMS memory owned_state validation dispatch ordering nested limits node_pool priority shutdown worker_policy worker_wake worker_wait worker_count worker_spin shutdown_running shutdown_waiter shutdown_queued shutdown_destroy shutdown_partial shutdown_cross_wait concurrent_wait) add_test(NAME guest_job_manager_${testCase} COMMAND guest_job_manager_tests ${testCase}) set_tests_properties(guest_job_manager_${testCase} PROPERTIES TIMEOUT 20) if(WIN32) diff --git a/core/libs/prx/libSceJobManager/Export.cpp b/core/libs/prx/libSceJobManager/Export.cpp index 4aa2e117ab..0901e79c0f 100644 --- a/core/libs/prx/libSceJobManager/Export.cpp +++ b/core/libs/prx/libSceJobManager/Export.cpp @@ -1,3 +1,5 @@ +#include "prx/libSceRazorCpu/libSceRazorCpu.hpp" +#include "prx/ulobjmgr/ulobjmgr.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include "prx/libkernel/include/Api.hpp" #include "prx/libSceJobManager/include/Api.hpp" @@ -16,13 +18,13 @@ #include #include #include +#include #include -#include #include +#include #include +#include #include -#include -#include #include "SceTypes.hpp" #include "ComputeProgress.hpp" @@ -106,19 +108,51 @@ class Sequence; enum class ItemType { Dispatch, SyncPoint, Wait, Compute, ComputeIndirect }; struct Item { - ItemType type; + Item* next = nullptr; + Item* runnableNext = nullptr; Sequence* sequence; - JobEntry entry; - void* argument; - std::uint32_t count; - std::uint32_t handedOut; - std::uint32_t completed; - bool* passed; - const void* shader = nullptr; - std::array groups{}; - const void* indirectArguments = nullptr; - std::string name; - bool named = false; + struct Cpu { + JobEntry entry; + void* argument; + std::uint32_t count; + std::uint32_t handedOut; + std::uint32_t completed; + }; + struct Compute { + const void* shader; + void* userData; + union { + std::array groups; + const void* indirectArguments; + }; + }; + union { + Cpu cpu; + Compute gpu; + bool* passed; + }; + const char* name = nullptr; + ItemType type; + bool encoded = false; +}; + +static_assert(sizeof(Item) == NodeBytes); + +struct RunQueue { + Item* first = nullptr; + Item* last = nullptr; + + void Push(Item* item) { + item->runnableNext = nullptr; + if (last != nullptr) last->runnableNext = item; + else first = item; + last = item; + } + + void Pop() { + first = first->runnableNext; + if (first == nullptr) last = nullptr; + } }; struct Counters { @@ -136,26 +170,32 @@ public: APS5_VABI int DispatchCompute(const void* shader, void* userData, std::int32_t x, std::int32_t y, std::int32_t z, const char* name) override; APS5_VABI int DispatchComputeIndirect(const void* shader, void* userData, const void* indirectArguments, const char* name) override; - bool Open(Manager* owner, const Counters& limit, std::uint32_t runQueue); + bool Open(Manager* owner, const Counters& limit, std::uint32_t runQueue, const char* name); int WaitAndClose(std::uint32_t index); void Advance(); void DispatchCompleted(std::uint32_t invocations); void CollectCompute(); void SetComputeStorage(std::uint32_t* commands, std::size_t words, Label* completion); - bool Busy() const { return outstanding != 0; } + void CancelWaits(); Manager* manager = nullptr; bool open = false; bool compute = false; std::uint32_t queue = 0; + std::uint16_t profileId = 0; private: int Latch(int code); void RequireOpen() const; int WaitLocked(std::unique_lock& guard); int AppendCompute(Item item); + void Append(Item* item); + void RemoveWait(Item* item); void SubmitCompute(const Item& item); + void SubmitComputeAhead(Item* item); + void RetireCompute(); void FlushCompute(); + void Profile(std::uint16_t event); Counters limits; Counters submitted; @@ -163,18 +203,42 @@ private: std::uint32_t priority = 0; int error = 0; std::uint32_t outstanding = 0; - std::size_t cursor = 0; - std::deque items; + Item* reserved = nullptr; + Item* first = nullptr; + Item* last = nullptr; + Item* cursor = nullptr; std::condition_variable passed; Label* label = nullptr; JobManager::ComputeProgress progress; std::uint32_t pendingCompute = 0; + std::uint32_t activeCompute = 0; + std::uint32_t availableCompute = 0; bool batchStarted = false; + bool profileActive = false; + bool profileCpu = false; + bool profileGpuBarrier = false; + const char* profileName = nullptr; std::uint32_t* commandBegin = nullptr; std::size_t commandWords = 0; std::size_t commandUsed = 0; }; +static_assert(sizeof(Sequence) < SequenceBytes); + +struct alignas(64) SequenceSlot { + Sequence value; + std::array padding; +}; + +static_assert(sizeof(SequenceSlot) == SequenceBytes); + +struct WorkerSlot { + Pthread thread = nullptr; + std::array padding; +}; + +static_assert(sizeof(WorkerSlot) == ThreadRecordBytes); + class Factory final : public ISequenceFactory { public: APS5_VABI int Create(ISequence** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char* name) override; @@ -187,11 +251,14 @@ public: }; struct Manager { + std::uint32_t objectId = 0; + std::uint16_t profileId = 0; std::mutex lock; std::condition_variable work; + std::condition_variable quiesced; bool running = false; std::uint32_t blockedWaiters = 0; - std::uint32_t freeNodes = 0; + Item* freeNodes = nullptr; std::uint32_t freeSequences = 0; std::uint32_t persistentThreads = 0; std::uint32_t workerCount = 0; @@ -199,18 +266,24 @@ struct Manager { std::uint32_t wakeTokens = 0; std::uint64_t pendingInvocations = 0; std::uint64_t spinTicks = 0; - std::array, 2> runQueues; - std::vector sequences; - std::deque computeSequences; + std::array runQueues; + std::span sequences; + std::span computeSequences; std::uint32_t freeComputeSequences = 0; - std::vector computeQueues; + std::uint64_t computeQueues = 0; KernelEqueue computeEvents = 0; Pthread computeHandler = nullptr; std::array factories; std::array unifiedFactories; - std::vector workers; + std::span workers; + std::uint32_t activeWorkers = 0; + void* stateBegin = nullptr; + std::size_t stateBytes = 0; + std::span nodes; }; +static_assert(sizeof(Manager) <= StateEnd - StateOffset); + thread_local Manager* currentWorker = nullptr; void WakeWorkers(Manager& manager, std::uint32_t invocations, bool publisherCanRun = true) { @@ -223,14 +296,18 @@ Manager*& Implementation(void* self) { return *static_cast(self); } -bool TakeNode(Manager& manager) { - if (manager.freeNodes == 0) return false; - --manager.freeNodes; - return true; +Item* TakeNode(Manager& manager) { + Item* item = manager.freeNodes; + if (item != nullptr) { + manager.freeNodes = item->next; + *item = {}; + } + return item; } -void ReturnNodes(Manager& manager, std::uint32_t count) { - manager.freeNodes += count; +void ReturnNode(Manager& manager, Item* item) { + item->next = manager.freeNodes; + manager.freeNodes = item; } int Sequence::Latch(int code) { @@ -238,7 +315,12 @@ int Sequence::Latch(int code) { return code; } -bool Sequence::Open(Manager* owner, const Counters& limit, std::uint32_t runQueue) { +void Sequence::Profile(std::uint16_t event) { + const auto id = static_cast(profileId - (compute && event <= 2 ? manager->sequences.size() : 0)); + static_cast(sceRazorCpuJobManagerSequence(profileName, manager->profileId, id, event)); +} + +bool Sequence::Open(Manager* owner, const Counters& limit, std::uint32_t runQueue, const char* name) { manager = owner; limits = limit; submitted = {}; @@ -246,12 +328,21 @@ bool Sequence::Open(Manager* owner, const Counters& limit, std::uint32_t runQueu priority = runQueue; error = 0; outstanding = 0; - cursor = 0; - items.clear(); + cursor = nullptr; + first = nullptr; + last = nullptr; commandUsed = 0; batchStarted = false; - if (!TakeNode(*owner)) return false; + activeCompute = 0; + availableCompute = 0; + profileActive = false; + profileCpu = false; + profileGpuBarrier = false; + profileName = name; + reserved = TakeNode(*owner); + if (reserved == nullptr) return false; open = true; + Profile(1); return true; } @@ -259,34 +350,92 @@ void Sequence::RequireOpen() const { if (!open) throw std::runtime_error("libSceJobManager: a destroyed sequence was used"); } +void Sequence::Append(Item* item) { + item->next = nullptr; + if (last != nullptr) last->next = item; + else first = item; + last = item; + if (cursor == nullptr) cursor = item; +} + +void Sequence::RemoveWait(Item* item) { + Item* previous = nullptr; + for (Item* current = first; current != nullptr; current = current->next) { + if (current == item) { + if (previous != nullptr) previous->next = item->next; + else first = item->next; + if (last == item) last = previous; + if (cursor == item) cursor = item->next; + return; + } + previous = current; + } +} + void Sequence::Advance() { - while (cursor < items.size()) { - Item& item = items[cursor]; + while (cursor != nullptr) { + Item& item = *cursor; if (item.type == ItemType::Dispatch) { + if (profileGpuBarrier) { + Profile(4); + profileActive = false; + profileGpuBarrier = false; + } + if (!profileActive) Profile(3); + profileActive = true; + profileCpu = true; + if (!manager->running) return; ++outstanding; - ++cursor; - manager->runQueues[priority].push_back(&item); - manager->pendingInvocations -= item.count; - WakeWorkers(*manager, item.count); + cursor = item.next; + manager->runQueues[priority].Push(&item); + manager->pendingInvocations -= item.cpu.count; + WakeWorkers(*manager, item.cpu.count); continue; } if (item.type == ItemType::Compute || item.type == ItemType::ComputeIndirect) { + if (!manager->running) return; + if (!profileActive) Profile(3); + profileActive = true; + profileGpuBarrier = false; ++outstanding; - ++cursor; - SubmitCompute(item); + ++activeCompute; + cursor = item.next; + if (!item.encoded) { + SubmitCompute(item); + item.encoded = true; + } + RetireCompute(); continue; } FlushCompute(); - if (outstanding != 0) return; + if (outstanding != 0) { + if (item.type == ItemType::SyncPoint && !profileCpu && manager->running) SubmitComputeAhead(item.next); + return; + } if (item.type == ItemType::SyncPoint) { + if (profileCpu) { + Profile(4); + profileActive = false; + profileCpu = false; + } + profileGpuBarrier = profileActive; ++completed.syncPoints; - ++cursor; + cursor = item.next; continue; } + if (profileActive) Profile(4); + profileActive = false; + profileCpu = false; + profileGpuBarrier = false; bool* const waiter = item.passed; - const auto retired = static_cast(cursor); - items.erase(items.begin(), items.begin() + static_cast(retired + 1)); - cursor = 0; + while (first != &item) { + Item* retired = first; + first = first->next; + ReturnNode(*manager, retired); + } + first = item.next; + if (last == &item) last = nullptr; + cursor = first; commandUsed = 0; batchStarted = false; if (error == 0) { @@ -296,7 +445,6 @@ void Sequence::Advance() { --submitted.waits; completed = {}; } - ReturnNodes(*manager, retired); *waiter = true; passed.notify_all(); } @@ -309,7 +457,14 @@ void Sequence::DispatchCompleted(std::uint32_t invocations) { if (outstanding == 0) Advance(); } -int Sequence::Dispatch(JobEntry entry, void* argument, std::uint32_t count, const char*) { +void Sequence::CancelWaits() { + for (Item* item = first; item != nullptr; item = item->next) { + if (item->type == ItemType::Wait) *item->passed = true; + } + passed.notify_all(); +} + +int Sequence::Dispatch(JobEntry entry, void* argument, std::uint32_t count, const char* name) { if (entry == nullptr) return SequenceNullArgument; if (count == 0 || count > MaxInvocations) return SequenceOutOfRange; const std::lock_guard guard(manager->lock); @@ -319,15 +474,20 @@ int Sequence::Dispatch(JobEntry entry, void* argument, std::uint32_t count, cons submitted.invocations += count; ++submitted.dispatches; if (before.invocations + count > limits.invocations || before.dispatches >= limits.dispatches) return Latch(SequenceLimitExceeded); - if (!TakeNode(*manager)) return Latch(SequenceNoNodes); - items.push_back({ItemType::Dispatch, this, entry, argument, count, 0, 0, nullptr}); + Item* item = TakeNode(*manager); + if (item == nullptr) return Latch(SequenceNoNodes); + item->type = ItemType::Dispatch; + item->sequence = this; + item->name = name; + item->cpu = {entry, argument, count, 0, 0}; + Append(item); manager->pendingInvocations += count; Advance(); return 0; } void Sequence::SubmitCompute(const Item& item) { - const auto* bytes = static_cast(item.shader); + const auto* bytes = static_cast(item.gpu.shader); const ShaderRegister* registers = nullptr; const std::uint8_t* specials = nullptr; std::memcpy(®isters, bytes + 0x20, sizeof(registers)); @@ -335,9 +495,9 @@ void Sequence::SubmitCompute(const Item& item) { const std::uint32_t registerCount = bytes[0x5c]; std::uint32_t modifier = 0; std::memcpy(&modifier, specials + 0x10, sizeof(modifier)); - const auto userData = reinterpret_cast(item.argument); + const auto userData = reinterpret_cast(item.gpu.userData); const std::array userDataWords{static_cast(userData), static_cast(userData >> 32)}; - const auto capacity = std::min(commandWords - commandUsed, 64 + 3 * registerCount + item.name.size() / 4); + const auto capacity = std::min(commandWords - commandUsed, 64 + 3 * registerCount + (item.name != nullptr ? std::strlen(item.name) / 4 : 0)); auto* const words = commandBegin + commandUsed; CommandBuffer buffer{words, words + capacity, words, words + capacity, nullptr, nullptr, 0}; { @@ -345,13 +505,13 @@ void Sequence::SubmitCompute(const Item& item) { bool encoded = sceAgcAcbAcquireMem(&buffer, 0x20380u | (batchStarted ? 0u : 1u), nullptr, 0, 0x190) != nullptr; encoded = encoded && sceAgcCbSetShRegistersDirect(&buffer, registers, registerCount) != nullptr; encoded = encoded && sceAgcCbSetShRegisterRangeDirect(&buffer, UserDataRegister, userDataWords.data(), 2) != nullptr; - if (item.named) encoded = encoded && sceAgcAcbPushMarker(&buffer, item.name.c_str(), 0) != nullptr; + if (item.name != nullptr) encoded = encoded && sceAgcAcbPushMarker(&buffer, item.name, 0) != nullptr; if (item.type == ItemType::Compute) { - encoded = encoded && sceAgcCbDispatch(&buffer, item.groups[0], item.groups[1], item.groups[2], modifier) != nullptr; + encoded = encoded && sceAgcCbDispatch(&buffer, item.gpu.groups[0], item.gpu.groups[1], item.gpu.groups[2], modifier) != nullptr; } else { - encoded = encoded && sceAgcAcbDispatchIndirect(&buffer, item.indirectArguments, modifier) != nullptr; + encoded = encoded && sceAgcAcbDispatchIndirect(&buffer, item.gpu.indirectArguments, modifier) != nullptr; } - if (item.named) encoded = encoded && sceAgcAcbPopMarker(&buffer) != nullptr; + if (item.name != nullptr) encoded = encoded && sceAgcAcbPopMarker(&buffer) != nullptr; if (!encoded) throw std::runtime_error("libSceJobManager: a compute dispatch did not fit its command buffer"); } batchStarted = true; @@ -369,6 +529,21 @@ void Sequence::SetComputeStorage(std::uint32_t* commands, std::size_t words, Lab label->value = 0; } +void Sequence::SubmitComputeAhead(Item* item) { + while (item != nullptr) { + if (item->type == ItemType::Compute || item->type == ItemType::ComputeIndirect) { + if (!item->encoded) { + SubmitCompute(*item); + item->encoded = true; + } + } else { + FlushCompute(); + if (item->type != ItemType::SyncPoint) return; + } + item = item->next; + } +} + void Sequence::FlushCompute() { if (pendingCompute == 0) return; if (commandWords - commandUsed < 8) throw std::runtime_error("libSceJobManager: a compute completion did not fit its command buffer"); @@ -386,11 +561,19 @@ void Sequence::FlushCompute() { if (sceAgcDriverSubmitAcb(queue, &packet) != 0) throw std::runtime_error("libSceJobManager: sceAgcDriverSubmitAcb failed for a completion"); } +void Sequence::RetireCompute() { + const auto finished = std::min(activeCompute, availableCompute); + activeCompute -= finished; + availableCompute -= finished; + completed.dispatches += finished; + outstanding -= finished; +} + void Sequence::CollectCompute() { const auto finished = progress.Collect(label->value); if (finished == 0) return; - completed.dispatches += finished; - outstanding -= finished; + availableCompute += finished; + RetireCompute(); if (outstanding == 0) Advance(); } @@ -400,8 +583,10 @@ int Sequence::AppendCompute(Item item) { if (!compute) throw std::runtime_error("libSceJobManager: a compute dispatch was made on a CPU sequence"); if (error != 0) return SequenceInvalid; if (submitted.dispatches++ >= limits.dispatches) return Latch(SequenceLimitExceeded); - if (!TakeNode(*manager)) return Latch(SequenceNoNodes); - items.push_back(std::move(item)); + Item* stored = TakeNode(*manager); + if (stored == nullptr) return Latch(SequenceNoNodes); + *stored = item; + Append(stored); Advance(); return 0; } @@ -409,22 +594,26 @@ int Sequence::AppendCompute(Item item) { int Sequence::DispatchCompute(const void* shader, void* userData, std::int32_t x, std::int32_t y, std::int32_t z, const char* name) { if (shader == nullptr) return SequenceNullArgument; if (x <= 0 || y <= 0 || z <= 0) return SequenceOutOfRange; - Item item{ItemType::Compute, this, nullptr, userData, 0, 0, 0, nullptr}; - item.shader = shader; - item.groups = {static_cast(x), static_cast(y), static_cast(z)}; - item.named = name != nullptr; - if (item.named) item.name = name; - return AppendCompute(std::move(item)); + Item item{}; + item.type = ItemType::Compute; + item.sequence = this; + item.gpu.shader = shader; + item.gpu.userData = userData; + item.gpu.groups = {static_cast(x), static_cast(y), static_cast(z)}; + item.name = name; + return AppendCompute(item); } int Sequence::DispatchComputeIndirect(const void* shader, void* userData, const void* indirectArguments, const char* name) { if (shader == nullptr || indirectArguments == nullptr || (reinterpret_cast(indirectArguments) & 0x1f) != 0) return SequenceNullArgument; - Item item{ItemType::ComputeIndirect, this, nullptr, userData, 0, 0, 0, nullptr}; - item.shader = shader; - item.indirectArguments = indirectArguments; - item.named = name != nullptr; - if (item.named) item.name = name; - return AppendCompute(std::move(item)); + Item item{}; + item.type = ItemType::ComputeIndirect; + item.sequence = this; + item.gpu.shader = shader; + item.gpu.userData = userData; + item.gpu.indirectArguments = indirectArguments; + item.name = name; + return AppendCompute(item); } int Sequence::SyncPoint() { @@ -433,8 +622,11 @@ int Sequence::SyncPoint() { if (error != 0) return SequenceInvalid; const std::uint32_t before = submitted.syncPoints++; if (before >= limits.syncPoints) return Latch(SequenceLimitExceeded); - if (!TakeNode(*manager)) return Latch(SequenceNoNodes); - items.push_back({ItemType::SyncPoint, this, nullptr, nullptr, 0, 0, 0, nullptr}); + Item* item = TakeNode(*manager); + if (item == nullptr) return Latch(SequenceNoNodes); + item->type = ItemType::SyncPoint; + item->sequence = this; + Append(item); Advance(); return 0; } @@ -447,13 +639,19 @@ int Sequence::Wait() { int Sequence::WaitAndClose(std::uint32_t index) { std::unique_lock guard(manager->lock); const int result = WaitLocked(guard); - const auto remaining = static_cast(std::count_if(items.begin(), items.end(), [](const Item& item) { return item.type != ItemType::Wait; })); - ReturnNodes(*manager, remaining + 1); - items.clear(); + while (first != nullptr) { + Item* item = first; + first = item->next; + if (item->type != ItemType::Wait) ReturnNode(*manager, item); + } + ReturnNode(*manager, reserved); + reserved = nullptr; + last = nullptr; commandUsed = 0; - cursor = 0; + cursor = nullptr; outstanding = 0; open = false; + Profile(2); (compute ? manager->freeComputeSequences : manager->freeSequences) |= std::uint32_t{1} << index; return result; } @@ -468,17 +666,23 @@ int Sequence::WaitLocked(std::unique_lock& guard) { } ++manager->blockedWaiters; bool done = false; - items.push_back({ItemType::Wait, this, nullptr, nullptr, 0, 0, 0, &done}); + Item waiter{}; + waiter.type = ItemType::Wait; + waiter.sequence = this; + waiter.passed = &done; + Append(&waiter); Advance(); if (!done && currentWorker == manager) { std::uint64_t runnable = 0; for (const auto& queue : manager->runQueues) { - for (const Item* item : queue) runnable += item->count - item->handedOut; + for (const Item* item = queue.first; item != nullptr; item = item->runnableNext) runnable += item->cpu.count - item->cpu.handedOut; } WakeWorkers(*manager, static_cast(std::min(runnable, std::uint64_t{manager->workerCount})), false); } passed.wait(guard, [&done] { return done; }); + RemoveWait(&waiter); --manager->blockedWaiters; + manager->quiesced.notify_all(); return result; } @@ -488,15 +692,15 @@ bool ValidLimits(std::uint32_t invocations, std::uint32_t dispatches, std::uint3 } template -int CreateIn(Manager* manager, TSequences& sequences, std::uint32_t& free, std::uint32_t priority, ISequence** out, const Counters& limits) { +int CreateIn(Manager* manager, TSequences& sequences, std::uint32_t& free, std::uint32_t priority, ISequence** out, const Counters& limits, const char* name) { if (out == nullptr) return SequenceNullArgument; if (!ValidLimits(limits.invocations, limits.dispatches, limits.syncPoints, limits.waits)) return SequenceOutOfRange; const std::lock_guard guard(manager->lock); if (free == 0) return SequenceNoneFree; const int index = std::countr_zero(free); free &= ~(std::uint32_t{1} << index); - Sequence& sequence = sequences[static_cast(index)]; - if (!sequence.Open(manager, limits, priority)) return SequenceNoNodes; + Sequence& sequence = sequences[static_cast(index)].value; + if (!sequence.Open(manager, limits, priority, name)) return SequenceNoNodes; *out = &sequence; return 0; } @@ -508,25 +712,25 @@ int DestroyIn(Manager* manager, TSequences& sequences, const std::uint32_t& free { const std::lock_guard guard(manager->lock); for (; index < sequences.size(); ++index) { - if (&sequences[index] == sequence) break; + if (&sequences[index].value == sequence) break; } if (index == sequences.size() || (free & (std::uint32_t{1} << index)) != 0) return SequenceInvalid; - found = &sequences[index]; + found = &sequences[index].value; } static_cast(found->WaitAndClose(index)); return 0; } -int Factory::Create(ISequence** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char*) { - return CreateIn(manager, manager->sequences, manager->freeSequences, priority, out, {invocations, dispatches, syncPoints, waits}); +int Factory::Create(ISequence** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char* name) { + return CreateIn(manager, manager->sequences, manager->freeSequences, priority, out, {invocations, dispatches, syncPoints, waits}, name); } int Factory::Destroy(ISequence* sequence) { return DestroyIn(manager, manager->sequences, manager->freeSequences, sequence); } -int Factory::CreateCompute(ISequence** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char*) { - return CreateIn(manager, manager->computeSequences, manager->freeComputeSequences, priority, out, {invocations, dispatches, syncPoints, waits}); +int Factory::CreateCompute(ISequence** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char* name) { + return CreateIn(manager, manager->computeSequences, manager->freeComputeSequences, priority, out, {invocations, dispatches, syncPoints, waits}, name); } int Factory::DestroyCompute(ISequence* sequence) { @@ -542,7 +746,7 @@ void* APS5_VABI ComputeHandlerEntry(void* argument) { if (count <= 0) continue; if (sceKernelGetEventFilter(&event) == UserEventFilter && sceKernelGetEventId(&event) == 0) return nullptr; const std::lock_guard guard(manager.lock); - for (Sequence& sequence : manager.computeSequences) sequence.CollectCompute(); + for (SequenceSlot& slot : manager.computeSequences) slot.value.CollectCompute(); } } @@ -550,19 +754,25 @@ bool RunOne(Manager& manager, std::unique_lock& guard) { Item* item = nullptr; for (const std::size_t queue : {std::size_t{1}, std::size_t{0}}) { auto& pending = manager.runQueues[queue]; - if (pending.empty()) continue; - item = pending.front(); - if (item->handedOut + 1 == item->count) pending.pop_front(); + if (pending.first == nullptr) continue; + item = pending.first; + if (item->cpu.handedOut + 1 == item->cpu.count) pending.Pop(); break; } if (item == nullptr) return false; - const std::uint32_t index = item->handedOut++; - const JobEntry entry = item->entry; - void* const argument = item->argument; + const std::uint32_t index = item->cpu.handedOut++; + const JobEntry entry = item->cpu.entry; + void* const argument = item->cpu.argument; + const auto dispatchId = static_cast(item - manager.nodes.data()); guard.unlock(); + static_cast(sceRazorCpuJobManagerJob(manager.profileId, dispatchId, static_cast(index), 16)); entry(argument, index); + static_cast(sceRazorCpuJobManagerJob(manager.profileId, dispatchId, static_cast(index), 32)); guard.lock(); - if (++item->completed == item->count) item->sequence->DispatchCompleted(item->count); + if (++item->cpu.completed == item->cpu.count) { + static_cast(sceRazorCpuJobManagerDispatch(item->name, manager.profileId, item->sequence->profileId, dispatchId, static_cast(item->cpu.count), reinterpret_cast(entry), reinterpret_cast(argument))); + item->sequence->DispatchCompleted(item->cpu.count); + } return true; } @@ -593,6 +803,10 @@ void* APS5_VABI WorkerEntry(void* argument) { } struct WorkLayout { + std::uint64_t workers = 0; + std::uint64_t sequences = 0; + std::uint64_t stateEnd = 0; + std::uint64_t pool = 0; std::uint64_t commands = 0; std::uint64_t commandBytes = 0; std::uint64_t labels = 0; @@ -602,9 +816,12 @@ struct WorkLayout { WorkLayout LayoutFor(std::uint64_t memory, std::uint64_t size, std::uint32_t workers, std::uint32_t sequences, std::uint32_t computeQueues) { WorkLayout layout; const std::uint64_t end = memory + size; - std::uint64_t address = AlignUp(memory + StateEnd, 8) + workers * ThreadRecordBytes; + layout.workers = AlignUp(memory + StateEnd, 8); + std::uint64_t address = layout.workers + workers * ThreadRecordBytes; for (const std::uint64_t bytes : {std::uint64_t{0x100}, std::uint64_t{0x80}, std::uint64_t{0x80}, std::uint64_t{0x80}}) address = AlignUp(address, 64) + bytes; - address = AlignUp(address, 64) + (sequences + computeQueues) * SequenceBytes; + layout.sequences = AlignUp(address, 64); + address = layout.sequences + (sequences + computeQueues) * SequenceBytes; + layout.stateEnd = address; if (computeQueues != 0) { layout.commandBytes = ComputeCommandBudget(); layout.commands = AlignUp(address + ComputeContextBytes, 0x100); @@ -612,6 +829,7 @@ WorkLayout LayoutFor(std::uint64_t memory, std::uint64_t size, std::uint32_t wor address = layout.labels + 8 * std::uint64_t{computeQueues}; } address = AlignUp(address, 64); + layout.pool = address; if (address <= end) layout.nodes = static_cast(((end - address) / NodeBytes) & 0xffff); return layout; } @@ -624,6 +842,17 @@ int ValidateWorkers(const ConfigParams& config, std::uint32_t workers) { return 0; } +class ThreadAttributes { +public: + ThreadAttributes() { + if (scePthreadAttrInit(&value) != 0) throw std::runtime_error("libSceJobManager: scePthreadAttrInit failed"); + } + ~ThreadAttributes() { static_cast(scePthreadAttrDestroy(&value)); } + ThreadAttributes(const ThreadAttributes&) = delete; + ThreadAttributes& operator=(const ThreadAttributes&) = delete; + PthreadAttr value = nullptr; +}; + void StartWorkers(Manager& manager, const ConfigParams& config, std::uint32_t workers) { std::uint32_t remaining = config.threadFlags & 0x1fff; for (std::uint32_t index = 0; index < workers; ++index) { @@ -632,20 +861,19 @@ void StartWorkers(Manager& manager, const ConfigParams& config, std::uint32_t wo affinity = remaining & (~remaining + 1); remaining &= remaining - 1; } - const std::string name = config.threadName != nullptr ? std::string(config.threadName).substr(0, 31) : "SceJobManagerWorker" + std::to_string(index); + std::array name{}; + if (config.threadName != nullptr) std::snprintf(name.data(), name.size(), "%.31s", config.threadName); + else std::snprintf(name.data(), name.size(), "SceJobManagerWorker%u", index); const KernelSchedParam schedule{static_cast(config.priority)}; - PthreadAttr attributes = nullptr; - if (scePthreadAttrInit(&attributes) != 0) throw std::runtime_error("libSceJobManager: scePthreadAttrInit failed"); - if (scePthreadAttrSetinheritsched(&attributes, 0) != 0 || scePthreadAttrSetschedparam(&attributes, &schedule) != 0 || - scePthreadAttrSetstacksize(&attributes, config.stackSize) != 0 || scePthreadAttrSetaffinity(&attributes, affinity) != 0) { + ThreadAttributes attributes; + if (scePthreadAttrSetinheritsched(&attributes.value, 0) != 0 || scePthreadAttrSetschedparam(&attributes.value, &schedule) != 0 || + scePthreadAttrSetstacksize(&attributes.value, config.stackSize) != 0 || scePthreadAttrSetaffinity(&attributes.value, affinity) != 0) { throw std::runtime_error("libSceJobManager: a worker thread attribute was rejected"); } Pthread thread = nullptr; - const int created = scePthreadCreate(&thread, &attributes, WorkerEntry, &manager, name.c_str()); - scePthreadAttrDestroy(&attributes); - + const int created = scePthreadCreate(&thread, &attributes.value, WorkerEntry, &manager, name.data()); if (created != 0) throw std::runtime_error("libSceJobManager: scePthreadCreate failed for a worker"); - manager.workers.push_back(thread); + manager.workers[manager.activeWorkers++].thread = thread; } } @@ -654,28 +882,67 @@ void StartCompute(Manager& manager, const WorkLayout& layout, std::uint32_t comp for (std::uint64_t mask = computeQueueMask; mask != 0; mask &= mask - 1) { const std::uint32_t queue = FirstComputeQueue + static_cast(std::countr_zero(mask)); if (sceAgcDriverAddEqEvent(manager.computeEvents, static_cast(queue), nullptr) != 0) throw std::runtime_error("libSceJobManager: sceAgcDriverAddEqEvent failed"); - manager.computeQueues.push_back(queue); + manager.computeQueues |= std::uint64_t{1} << (queue - FirstComputeQueue); } - manager.computeSequences.resize(computeSequences); + std::uint64_t availableQueues = computeQueueMask; for (std::uint32_t index = 0; index < computeSequences; ++index) { - manager.computeSequences[index].compute = true; - manager.computeSequences[index].queue = manager.computeQueues[index]; - manager.computeSequences[index].SetComputeStorage(reinterpret_cast(layout.commands + index * layout.commandBytes), + Sequence& sequence = manager.computeSequences[index].value; + sequence.compute = true; + sequence.queue = FirstComputeQueue + static_cast(std::countr_zero(availableQueues)); + availableQueues &= availableQueues - 1; + sequence.SetComputeStorage(reinterpret_cast(layout.commands + index * layout.commandBytes), static_cast(layout.commandBytes / 4), reinterpret_cast(layout.labels + index * sizeof(Label))); } manager.freeComputeSequences = computeSequences == MaxSequences ? ~std::uint32_t{0} : (std::uint32_t{1} << computeSequences) - 1; const KernelSchedParam schedule{static_cast(priority)}; - PthreadAttr attributes = nullptr; - if (scePthreadAttrInit(&attributes) != 0 || scePthreadAttrSetinheritsched(&attributes, 0) != 0 || scePthreadAttrSetschedparam(&attributes, &schedule) != 0 || - scePthreadAttrSetstacksize(&attributes, 0x4000) != 0 || scePthreadAttrSetaffinity(&attributes, affinity) != 0) { + ThreadAttributes attributes; + if (scePthreadAttrSetinheritsched(&attributes.value, 0) != 0 || scePthreadAttrSetschedparam(&attributes.value, &schedule) != 0 || + scePthreadAttrSetstacksize(&attributes.value, 0x4000) != 0 || scePthreadAttrSetaffinity(&attributes.value, affinity) != 0) { throw std::runtime_error("libSceJobManager: a compute handler thread attribute was rejected"); } - const int created = scePthreadCreate(&manager.computeHandler, &attributes, ComputeHandlerEntry, &manager, "SceJobManagerComputeEventHdlr"); - scePthreadAttrDestroy(&attributes); - + const int created = scePthreadCreate(&manager.computeHandler, &attributes.value, ComputeHandlerEntry, &manager, "SceJobManagerComputeEventHdlr"); if (created != 0) throw std::runtime_error("libSceJobManager: scePthreadCreate failed for the compute handler"); } +void StopManager(Manager& manager) { + { + const std::lock_guard guard(manager.lock); + manager.running = false; + manager.work.notify_all(); + } + while (manager.activeWorkers != 0) { + if (scePthreadJoin(manager.workers[manager.activeWorkers - 1].thread, nullptr) != 0) throw std::runtime_error("sce::Job::JobManager::shutdown: scePthreadJoin failed"); + manager.workers[--manager.activeWorkers].thread = nullptr; + } + if (manager.computeHandler != nullptr) { + if (sceKernelAddUserEventEdge(manager.computeEvents, 0) != 0 || sceKernelTriggerUserEvent(manager.computeEvents, 0, nullptr) != 0) { + throw std::runtime_error("sce::Job::JobManager::shutdown: the compute handler could not be signalled"); + } + if (scePthreadJoin(manager.computeHandler, nullptr) != 0) throw std::runtime_error("sce::Job::JobManager::shutdown: scePthreadJoin failed"); + manager.computeHandler = nullptr; + } + if (manager.computeEvents != 0) { + for (std::uint64_t mask = manager.computeQueues; mask != 0; mask &= mask - 1) { + const auto queue = FirstComputeQueue + static_cast(std::countr_zero(mask)); + static_cast(sceAgcDriverDeleteEqEvent(manager.computeEvents, static_cast(queue))); + } + if (sceKernelDeleteEqueue(manager.computeEvents) != 0) throw std::runtime_error("sce::Job::JobManager::shutdown: sceKernelDeleteEqueue failed"); + manager.computeEvents = 0; + manager.computeQueues = 0; + } + std::unique_lock guard(manager.lock); + for (SequenceSlot& slot : manager.sequences) slot.value.CancelWaits(); + for (SequenceSlot& slot : manager.computeSequences) slot.value.CancelWaits(); + manager.quiesced.wait(guard, [&manager] { return manager.blockedWaiters == 0; }); +} + +void DestroyManager(Manager* manager) { + const GuestArena::HostWrite memory(manager->stateBegin, manager->stateBytes); + for (SequenceSlot& slot : manager->sequences) std::destroy_at(&slot); + for (SequenceSlot& slot : manager->computeSequences) std::destroy_at(&slot); + std::destroy_at(manager); +} + int Initialize(void* self, void* memory, std::uint64_t size, std::uint32_t workers, std::uint32_t sequences, std::uint32_t computeSequences, std::uint64_t computeQueueMask, const ConfigParams* config, bool unified) { if (Implementation(self) != nullptr) return ManagerAlreadyInitialized; if (memory == nullptr) return ManagerNullMemory; @@ -705,23 +972,60 @@ int Initialize(void* self, void* memory, std::uint64_t size, std::uint32_t worke const std::uint32_t handlerPriority = ownHandlerSettings ? settings.computePriority : 0x100; const std::uint32_t handlerAffinity = ownHandlerSettings ? settings.computeAffinity : 0x1fff; if (computeSequences != 0 && (handlerPriority < 0x100 || handlerPriority > 0x2ff || handlerAffinity == 0 || handlerAffinity > 0x1fff)) return ManagerInvalidArgument; - auto* manager = new Manager; + const GuestArena::HostWrite stateMemory(memory, static_cast(layout.stateEnd - base)); + const GuestArena::HostWrite nodeMemory(reinterpret_cast(layout.pool), layout.nodes * sizeof(Item)); + Manager* manager = std::construct_at(reinterpret_cast(base + StateOffset)); + manager->stateBegin = memory; + manager->stateBytes = static_cast(layout.stateEnd - base); + manager->nodes = {reinterpret_cast(layout.pool), layout.nodes}; + std::unique_ptr owner(manager, DestroyManager); manager->running = true; manager->workerCount = workers; manager->spinTicks = sceKernelGetTscFrequency() / 1000000 * settings.spinMicroseconds; - manager->freeNodes = layout.nodes - 1; + for (std::uint32_t index = 0; index < layout.nodes; ++index) std::construct_at(&manager->nodes[index]); + for (std::uint32_t index = layout.nodes; index > 1; --index) ReturnNode(*manager, &manager->nodes[index - 1]); manager->freeSequences = sequences == MaxSequences ? ~std::uint32_t{0} : (std::uint32_t{1} << sequences) - 1; - manager->sequences = std::vector(sequences); + manager->workers = {reinterpret_cast(layout.workers), workers}; + for (WorkerSlot& worker : manager->workers) std::construct_at(&worker); + auto* cpuSlots = reinterpret_cast(layout.sequences); + auto* computeSlots = cpuSlots + sequences; + for (std::uint32_t index = 0; index < sequences; ++index) { + std::construct_at(cpuSlots + index); + cpuSlots[index].value.profileId = static_cast(index); + manager->sequences = {cpuSlots, index + 1}; + } + for (std::uint32_t index = 0; index < computeSequences; ++index) { + std::construct_at(computeSlots + index); + computeSlots[index].value.profileId = static_cast(sequences + index); + manager->computeSequences = {computeSlots, index + 1}; + } for (std::uint32_t priority = 0; priority < 2; ++priority) { manager->factories[priority].manager = manager; manager->factories[priority].priority = priority; manager->unifiedFactories[priority].manager = manager; manager->unifiedFactories[priority].priority = priority; } - if (computeSequences != 0) StartCompute(*manager, layout, computeSequences, computeQueueMask, handlerPriority, handlerAffinity); - Implementation(self) = manager; - StartWorkers(*manager, settings, workers); - return 0; + try { + if (computeSequences != 0) StartCompute(*manager, layout, computeSequences, computeQueueMask, handlerPriority, handlerAffinity); + StartWorkers(*manager, settings, workers); + } catch (...) { + try { + StopManager(*manager); + } catch (...) { + Implementation(self) = owner.release(); + throw; + } + throw; + } + Implementation(self) = owner.release(); + const int result = _sceUlobjmgrRegisterObject_nid_postfix(base, 0x21, &manager->objectId); + std::uint32_t crc = 0; + for (std::uint32_t byte = 0; byte < sizeof(base); ++byte) { + crc ^= static_cast(base >> (byte * 8)); + for (std::uint32_t bit = 0; bit < 8; ++bit) crc = (crc >> 1) ^ (0x82f63b78u & (0u - (crc & 1))); + } + manager->profileId = static_cast(crc >> 16); + return result; } ISequenceFactory* FactoryFor(void* self, std::uint32_t priority, bool unified) { @@ -795,28 +1099,15 @@ int APS5_VABI _ZN3sce3Job10JobManager10initializeEPvmjjPKNS1_12ConfigParamsE(voi int APS5_VABI _ZN3sce3Job10JobManager8shutdownEv(void* self) { Manager* manager = Implementation(self); if (manager == nullptr) return ManagerNotInitialized; + if (currentWorker == manager) throw std::runtime_error("sce::Job::JobManager::shutdown: a worker cannot join itself"); { const std::lock_guard guard(manager->lock); - if (manager->blockedWaiters != 0) throw std::runtime_error("sce::Job::JobManager::shutdown: a thread is still blocked in a sequence wait"); - for (const Sequence& sequence : manager->computeSequences) { - if (sequence.open && sequence.Busy()) throw std::runtime_error("sce::Job::JobManager::shutdown: a compute sequence still has work in flight"); - } manager->running = false; - manager->work.notify_all(); - } - for (const Pthread worker : manager->workers) { - if (scePthreadJoin(worker, nullptr) != 0) throw std::runtime_error("sce::Job::JobManager::shutdown: scePthreadJoin failed"); - } - if (manager->computeHandler != nullptr) { - if (sceKernelAddUserEventEdge(manager->computeEvents, 0) != 0 || sceKernelTriggerUserEvent(manager->computeEvents, 0, nullptr) != 0) { - throw std::runtime_error("sce::Job::JobManager::shutdown: the compute handler could not be signalled"); - } - if (scePthreadJoin(manager->computeHandler, nullptr) != 0) throw std::runtime_error("sce::Job::JobManager::shutdown: scePthreadJoin failed"); - for (const std::uint32_t queue : manager->computeQueues) static_cast(sceAgcDriverDeleteEqEvent(manager->computeEvents, static_cast(queue))); - if (sceKernelDeleteEqueue(manager->computeEvents) != 0) throw std::runtime_error("sce::Job::JobManager::shutdown: sceKernelDeleteEqueue failed"); } + StopManager(*manager); + static_cast(_sceUlobjmgrUnregisterObject_nid_postfix(manager->objectId)); Implementation(self) = nullptr; - delete manager; + DestroyManager(manager); return 0; } diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 7e594babab..477b3d020f 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -492,10 +492,9 @@ private: skip(); if (peek() >= 0 && peek() == ']') { take(); --depth; return true; } for (;;) { - Value item{}; + auto& item = out.array.items->emplace_back(); Construct(item); item.node->handlers = handlers; - out.array.items->push_back(item); skip(); if (!value(*item.node)) return false; skip(); diff --git a/core/libs/prx/libSceJxr/CMakeLists.txt b/core/libs/prx/libSceJxr/CMakeLists.txt index 251402ba35..c94b793736 100644 --- a/core/libs/prx/libSceJxr/CMakeLists.txt +++ b/core/libs/prx/libSceJxr/CMakeLists.txt @@ -1,8 +1,51 @@ -add_library(libSceJxr SHARED EXCLUDE_FROM_ALL Export.cpp) +add_library(jxr_lookup_native STATIC src/NativeLookup.c) +configure_jxr_native(jxr_lookup_native) +target_include_directories(jxr_lookup_native PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(jxr_lookup_native PRIVATE jxr) +add_library(libSceJxr SHARED EXCLUDE_FROM_ALL Export.cpp src/PixelFormatLookup.cpp) +target_link_libraries(libSceJxr PRIVATE jxr_lookup_native) +if(NOT APPLE) + target_link_options(libSceJxr PRIVATE LINKER:--exclude-libs,ALL) +endif() target_include_directories(libSceJxr PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceJxr) if(BUILD_TESTING) + add_library(jxr_lookup_reference STATIC ${LIBS_INCLUDE_DIR}/tests/GuestJxrLookupReference.c) + configure_jxr_native(jxr_lookup_reference) + target_link_libraries(jxr_lookup_reference PRIVATE jxr) + add_test_executable(guest_jxr_lookup_tests ${LIBS_INCLUDE_DIR}/tests/GuestJxrLookup.cpp) + target_include_directories(guest_jxr_lookup_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_jxr_lookup_tests PRIVATE jxr_lookup_reference ${CMAKE_DL_LIBS}) + if(WIN32) + target_link_libraries(guest_jxr_lookup_tests PRIVATE libc) + endif() + configure_windows_unwind(guest_jxr_lookup_tests) + add_custom_target(jxr_lookup_patched DEPENDS ${PATCHED_DIR}/libSceJxr.prx ${PATCHED_DIR}/libkernel.prx ${LIBC_PATCHED}) + add_dependencies(guest_jxr_lookup_tests jxr_lookup_patched) + add_test(NAME guest_jxr_lookup_reference COMMAND guest_jxr_lookup_tests reference) + add_test(NAME guest_jxr_lookup COMMAND guest_jxr_lookup_tests $ raw) + add_test(NAME guest_jxr_lookup_patched COMMAND guest_jxr_lookup_tests ${PATCHED_DIR}/libSceJxr.prx patched) + set_tests_properties(guest_jxr_lookup_reference guest_jxr_lookup guest_jxr_lookup_patched PROPERTIES TIMEOUT 20) + foreach(direction IN ITEMS forward reverse) + add_test(NAME guest_jxr_lookup_padding_${direction} + COMMAND guest_jxr_lookup_tests $ raw ${direction}) + add_test(NAME guest_jxr_lookup_padding_${direction}_patched + COMMAND guest_jxr_lookup_tests ${PATCHED_DIR}/libSceJxr.prx patched ${direction}) + set_tests_properties(guest_jxr_lookup_padding_${direction} guest_jxr_lookup_padding_${direction}_patched PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_jxr_lookup_padding_${direction} PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(guest_jxr_lookup_padding_${direction}_patched PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:${PATCHED_DIR}") + endif() + endforeach() + if(WIN32) + set_tests_properties(guest_jxr_lookup_reference guest_jxr_lookup PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(guest_jxr_lookup_patched PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:${PATCHED_DIR}") + endif() add_test_executable(guest_jxr_tests ${LIBS_INCLUDE_DIR}/tests/GuestJxr.cpp) target_include_directories(guest_jxr_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_jxr_tests PRIVATE libSceJxr libc) @@ -13,3 +56,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_jxr_stream_tests ${LIBS_INCLUDE_DIR}/tests/JxrStream.cpp) + target_include_directories(guest_jxr_stream_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_jxr_stream_tests PRIVATE libSceJxr libc libkernel) + configure_windows_unwind(guest_jxr_stream_tests) + add_test(NAME guest_jxr_stream COMMAND guest_jxr_stream_tests) + set_tests_properties(guest_jxr_stream PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_jxr_stream PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceJxr/Export.cpp b/core/libs/prx/libSceJxr/Export.cpp index bd2404ed30..df9d322a78 100644 --- a/core/libs/prx/libSceJxr/Export.cpp +++ b/core/libs/prx/libSceJxr/Export.cpp @@ -2,6 +2,9 @@ #include "prx/libSceJxr/include/Api.hpp" #include #include +#include +#include "Stream.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -21,19 +24,38 @@ std::int64_t APS5_VABI PKFree_nid_postfix(void** memory) { return WmpErrSuccess; } -int APS5_VABI PixelFormatLookup_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI readIS_nid_postfix(void*, Jxr::BitIo* io) { + if (!io) NotImplemented_nid_no_patch(__func__); + constexpr std::uintptr_t packetSize = 4096; + const auto start = reinterpret_cast(io->start); + if (((start ^ reinterpret_cast(io->current)) & packetSize) == 0) return WmpErrSuccess; + auto* stream = io->stream; + if (!stream || !stream->setPosition) NotImplemented_nid_no_patch(__func__); + stream->setPosition(stream, io->referenceOffset); + if (!io->start || !stream->read) NotImplemented_nid_no_patch(__func__); + { + GuestArena::HostWrite access(io->start, packetSize); + if (!access.Open()) NotImplemented_nid_no_patch(__func__); + stream->read(stream, io->start, packetSize); + } + io->referenceOffset += packetSize; + std::memcpy(&io->shadow, io->start, sizeof(io->shadow)); + const auto mask = static_cast(static_cast(io->mask)); + io->start = reinterpret_cast((reinterpret_cast(io->start) + packetSize) & mask); + return WmpErrSuccess; } -int APS5_VABI readIS_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI writeIS_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI writeIS_nid_postfix(void*, Jxr::BitIo* io) { + if (!io) NotImplemented_nid_no_patch(__func__); + constexpr std::uintptr_t packetSize = 4096; + const auto start = reinterpret_cast(io->start); + if (((start ^ reinterpret_cast(io->current)) & packetSize) == 0) return WmpErrSuccess; + if (!io->start || !io->stream || !io->stream->write) NotImplemented_nid_no_patch(__func__); + const auto result = io->stream->write(io->stream, io->start, packetSize); + if (result < 0) return result; + const auto mask = static_cast(static_cast(io->mask)); + io->start = reinterpret_cast((reinterpret_cast(io->start) + packetSize) & mask); + return result; } } diff --git a/core/libs/prx/libSceJxr/GuestPixelInfo.h b/core/libs/prx/libSceJxr/GuestPixelInfo.h new file mode 100644 index 0000000000..95a9893d0a --- /dev/null +++ b/core/libs/prx/libSceJxr/GuestPixelInfo.h @@ -0,0 +1,15 @@ +#pragma once +#include + +typedef struct JxrPixelInfo { + const void* guid; + uint64_t channels; + int32_t colorFormat; + int32_t bitDepth; + uint32_t bitsPerUnit; + uint64_t flags; + uint32_t interpretation; + uint32_t samplesPerPixel; + uint32_t bitsPerSample; + uint32_t sampleFormat; +} JxrPixelInfo; diff --git a/core/libs/prx/libSceJxr/Stream.hpp b/core/libs/prx/libSceJxr/Stream.hpp new file mode 100644 index 0000000000..20af71092e --- /dev/null +++ b/core/libs/prx/libSceJxr/Stream.hpp @@ -0,0 +1,38 @@ +#ifndef CORE_LIBS_PRX_LIBSCEJXR_STREAM_HPP +#define CORE_LIBS_PRX_LIBSCEJXR_STREAM_HPP + +#include +#include +#include +#include "prx/libc/include/general/VabiMacros.hpp" + +namespace Jxr { + +struct Stream { + std::array state; + std::int32_t memory; + std::int64_t (APS5_VABI* close)(Stream**); + std::int32_t (APS5_VABI* end)(Stream*); + std::int64_t (APS5_VABI* read)(Stream*, void*, std::size_t); + std::int64_t (APS5_VABI* write)(Stream*, const void*, std::size_t); + std::int64_t (APS5_VABI* setPosition)(Stream*, std::size_t); + std::int64_t (APS5_VABI* getPosition)(Stream*, std::size_t*); +}; + +struct BitIo { + std::uint32_t shadow; + std::uint32_t accumulator; + std::uint32_t bitsUsed; + std::int32_t mask; + std::uint8_t* start; + std::uint8_t* current; + Stream* stream; + std::size_t referenceOffset; +}; + +static_assert(sizeof(Stream) == 88 && offsetof(Stream, read) == 56 && offsetof(Stream, setPosition) == 72); +static_assert(sizeof(BitIo) == 48 && offsetof(BitIo, start) == 16 && offsetof(BitIo, referenceOffset) == 40); + +} // namespace Jxr + +#endif diff --git a/core/libs/prx/libSceJxr/include/Api.hpp b/core/libs/prx/libSceJxr/include/Api.hpp index b29df513d6..51d874f9c5 100644 --- a/core/libs/prx/libSceJxr/include/Api.hpp +++ b/core/libs/prx/libSceJxr/include/Api.hpp @@ -1,12 +1,14 @@ #pragma once #include "SceTypes.hpp" +#include "prx/libSceJxr/GuestPixelInfo.h" +#include "prx/libSceJxr/Stream.hpp" extern "C" { std::int64_t APS5_VABI PKFree_nid_postfix(void** memory); -int APS5_VABI PixelFormatLookup_nid_postfix(void); -int APS5_VABI readIS_nid_postfix(void); -int APS5_VABI writeIS_nid_postfix(void); +std::int64_t APS5_VABI PixelFormatLookup_nid_postfix(JxrPixelInfo* value, std::uint8_t mode); +std::int64_t APS5_VABI readIS_nid_postfix(void* codec, Jxr::BitIo* io); +std::int64_t APS5_VABI writeIS_nid_postfix(void* codec, Jxr::BitIo* io); } diff --git a/core/libs/prx/libSceJxr/src/NativeLookup.c b/core/libs/prx/libSceJxr/src/NativeLookup.c new file mode 100644 index 0000000000..086cc753cf --- /dev/null +++ b/core/libs/prx/libSceJxr/src/NativeLookup.c @@ -0,0 +1,40 @@ +#include "strcodec.h" +#include "JXRGlue.h" +#include "prx/libSceJxr/GuestPixelInfo.h" +#include +#include + +_Static_assert(sizeof(GUID) == 16, "GUID size"); +_Static_assert(sizeof(COLORFORMAT) == 4 && sizeof(BITDEPTH_BITS) == 4, "enum size"); +_Static_assert(sizeof(U32) == 4 && sizeof(JxrPixelInfo) == 56, "pixel info layout"); +_Static_assert(offsetof(JxrPixelInfo, flags) == 32 && offsetof(JxrPixelInfo, interpretation) == 40, "LP64 offsets"); + +int64_t JxrLookup_nid_no_patch(JxrPixelInfo* value, uint8_t mode) { + PKPixelInfo native = {0}; + GUID guid; + ERR result; + if (mode > LOOKUP_BACKWARD_TIF) return PixelFormatLookup(NULL, mode); + if (mode == LOOKUP_FORWARD) { + memcpy(&guid, value->guid, sizeof(guid)); + native.pGUIDPixFmt = &guid; + } else { + native.grBit = (WMP_GRBIT)value->flags; + native.uInterpretation = value->interpretation; + native.uSamplePerPixel = value->samplesPerPixel; + native.uBitsPerSample = value->bitsPerSample; + native.uSampleFormat = value->sampleFormat; + } + result = PixelFormatLookup(&native, mode); + if (result != WMP_errSuccess) return result; + value->guid = native.pGUIDPixFmt; + value->channels = native.cChannel; + value->colorFormat = native.cfColorFormat; + value->bitDepth = native.bdBitDepth; + value->bitsPerUnit = native.cbitUnit; + value->flags = native.grBit; + value->interpretation = native.uInterpretation; + value->samplesPerPixel = native.uSamplePerPixel; + value->bitsPerSample = native.uBitsPerSample; + value->sampleFormat = native.uSampleFormat; + return result; +} diff --git a/core/libs/prx/libSceJxr/src/PixelFormatLookup.cpp b/core/libs/prx/libSceJxr/src/PixelFormatLookup.cpp new file mode 100644 index 0000000000..73f6d4000c --- /dev/null +++ b/core/libs/prx/libSceJxr/src/PixelFormatLookup.cpp @@ -0,0 +1,35 @@ +#include "prx/libSceJxr/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" std::int64_t JxrLookup_nid_no_patch(JxrPixelInfo* value, std::uint8_t mode); + +static_assert(sizeof(JxrPixelInfo) == 56 && alignof(JxrPixelInfo) == 8); +static_assert(offsetof(JxrPixelInfo, flags) == 32 && offsetof(JxrPixelInfo, interpretation) == 40); + +extern "C" std::int64_t APS5_VABI PixelFormatLookup_nid_postfix(JxrPixelInfo* value, std::uint8_t mode) { + if (mode > 1) return JxrLookup_nid_no_patch(nullptr, mode); + if (value == nullptr) throw std::invalid_argument("PixelFormatLookup: null record behavior is unknown"); + JxrPixelInfo local{}; + auto* bytes = reinterpret_cast(value); + if (mode == 0) { + std::memcpy(&local.guid, bytes, sizeof(local.guid)); + if (local.guid == nullptr) throw std::invalid_argument("PixelFormatLookup: null GUID behavior is unknown"); + } else { + std::memcpy(&local.flags, bytes + offsetof(JxrPixelInfo, flags), sizeof(local.flags)); + std::memcpy(&local.interpretation, bytes + offsetof(JxrPixelInfo, interpretation), 4 * sizeof(std::uint32_t)); + } + const auto result = JxrLookup_nid_no_patch(&local, mode); + if (result == 0) { + constexpr auto paddingBegin = offsetof(JxrPixelInfo, bitsPerUnit) + sizeof(local.bitsPerUnit); + constexpr auto paddingSize = offsetof(JxrPixelInfo, flags) - paddingBegin; + static_assert(paddingBegin == 28 && paddingSize == 4); + std::memcpy(bytes, &local, paddingBegin); + std::memset(bytes + paddingBegin, 0, paddingSize); + std::memcpy(bytes + offsetof(JxrPixelInfo, flags), &local.flags, sizeof(local) - offsetof(JxrPixelInfo, flags)); + } + return result; +} diff --git a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp index 954040b147..ba9d9bfff8 100644 --- a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp +++ b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp @@ -175,17 +175,15 @@ int GetKey2Char(std::int32_t handle, std::int32_t arrange, std::uint32_t ledStat { std::lock_guard lock(keyboardMutex); if (!initialized) return KEYBOARD_ERROR_NOT_INITIALIZED; + if (keyCode < 4 || keyCode > 231) return KEYBOARD_ERROR_INVALID_ARG; if (handle != KEYBOARD_HANDLE || !opened) return KEYBOARD_ERROR_INVALID_HANDLE; } if (charData == nullptr) return KEYBOARD_ERROR_INVALID_ARG; if (arrange != KEYBOARD_ARRANGEMENT_101 && arrange != KEYBOARD_ARRANGEMENT_106) return KEYBOARD_ERROR_INVALID_ARG; - if (arrange == KEYBOARD_ARRANGEMENT_106 && (ledState & KEYBOARD_LED_KANA) != 0) { - NotImplemented_nid_no_patch(__func__); - return KEYBOARD_ERROR_INVALID_ARG; - } *charData = {}; - charData->char_code = character(arrange == KEYBOARD_ARRANGEMENT_106, ledState, modifierKey, keyCode); - charData->processed = charData->char_code != 0; + const std::uint16_t converted = character(arrange == KEYBOARD_ARRANGEMENT_106, ledState, modifierKey, keyCode); + charData->char_code = converted != 0 ? converted : keyCode; + charData->processed = converted != 0; charData->length = charData->processed ? 1 : 0; return KEYBOARD_OK; } diff --git a/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp b/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp index 56b97a8d39..e9856f2c1a 100644 --- a/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp +++ b/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp @@ -5,10 +5,18 @@ #include "SDL_events.h" #include "SDL_keyboard.h" +#include #include +#include +#include +#include #include -static void Require(bool value) { if (!value) std::abort(); } +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "%s:%u: keyboard assertion failed\n", location.file_name(), location.line()); + std::abort(); +} static SDL_Event KeyEvent(Uint32 type, SDL_Scancode scancode, Uint16 mod = 0, Uint8 repeat = 0) { SDL_Event event{}; @@ -23,14 +31,63 @@ static SDL_Event KeyEvent(Uint32 type, SDL_Scancode scancode, Uint16 mod = 0, Ui static std::uint16_t Char(std::uint32_t led, std::uint32_t modifierKey, std::uint16_t keyCode, std::int32_t arrange = KEYBOARD_ARRANGEMENT_101) { KeyboardCharData data{}; Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, arrange, led, modifierKey, keyCode, &data) == KEYBOARD_OK); - Require(data.processed == (data.char_code != 0) && data.length == (data.processed ? 1 : 0)); - return data.char_code; + Require(data.processed ? data.char_code != 0 : data.char_code == keyCode); + Require(data.length == (data.processed ? 1 : 0)); + return data.processed ? data.char_code : 0; } static std::uint16_t Jis(std::uint32_t modifierKey, std::uint16_t keyCode, std::uint32_t led = 0) { return Char(led, modifierKey, keyCode, KEYBOARD_ARRANGEMENT_106); } +static void CheckInvalidKeys(bool opened) { + constexpr std::uint16_t invalidKeys[] = {0, 1, 2, 3, 232, 255, 256, 65535}; + for (const auto key : invalidKeys) { + for (const auto handle : {KEYBOARD_HANDLE, 42}) { + KeyboardCharData data; + std::memset(&data, 0xa5, sizeof(data)); + unsigned char before[sizeof(data)]; + std::memcpy(before, &data, sizeof(data)); + Require(sceKeyboardGetKey2Char(handle, KEYBOARD_ARRANGEMENT_101, 0, 0, key, &data) == KEYBOARD_ERROR_INVALID_ARG); + Require(std::memcmp(&data, before, sizeof(data)) == 0); + Require(sceKeyboardGetKey2Char(handle, KEYBOARD_ARRANGEMENT_106, KEYBOARD_LED_KANA, 0, key, nullptr) == KEYBOARD_ERROR_INVALID_ARG); + } + } + for (const auto key : {std::uint16_t{4}, std::uint16_t{231}}) { + KeyboardCharData data{}; + const auto expected = opened ? KEYBOARD_OK : KEYBOARD_ERROR_INVALID_HANDLE; + Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, KEYBOARD_ARRANGEMENT_101, 0, 0, key, &data) == expected); + } +} + +static void CheckUnconvertedKeys() { + constexpr std::uint16_t unconvertedKeys[] = {0x29, 0x39, 0x3a, 0x4f, 0x59, 0x88, 0xe0, 0xe7}; + for (const auto arrange : {KEYBOARD_ARRANGEMENT_101, KEYBOARD_ARRANGEMENT_106}) { + for (const auto key : unconvertedKeys) { + KeyboardCharData data{}; + Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, arrange, 0, 0, key, &data) == KEYBOARD_OK); + Require(!data.processed && data.char_code == key); + } + } +} + +static void CheckKanaLed() { + for (const auto arrange : {KEYBOARD_ARRANGEMENT_101, KEYBOARD_ARRANGEMENT_106}) { + for (const auto modifier : {std::uint8_t{0}, std::uint8_t{KEYBOARD_MOD_LEFT_SHIFT}, std::uint8_t{KEYBOARD_MOD_RIGHT_SHIFT}}) { + for (const auto led : {std::uint8_t{0}, std::uint8_t{KEYBOARD_LED_NUM_LOCK | KEYBOARD_LED_CAPS_LOCK}}) { + for (std::uint16_t key = 4; key <= 231; ++key) { + KeyboardCharData withoutKana{}; + KeyboardCharData withKana{}; + Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, arrange, led, modifier, key, &withoutKana) == KEYBOARD_OK); + Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, arrange, led | KEYBOARD_LED_KANA, modifier, key, &withKana) == KEYBOARD_OK); + Require(withKana.processed == withoutKana.processed); + Require(withKana.length == withoutKana.length && withKana.char_code == withoutKana.char_code); + } + } + } + } +} + int main() { KeyboardData data[16]{}; KeyboardCharData charData{}; @@ -39,11 +96,15 @@ int main() { Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, &connected) == KEYBOARD_ERROR_NOT_INITIALIZED); Require(sceKeyboardInit() == KEYBOARD_OK); Require(sceKeyboardInit() == KEYBOARD_OK); + CheckInvalidKeys(false); Require(sceKeyboardOpen(1, 0, 1, nullptr) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardOpen(1, 1, 0, nullptr) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, KEYBOARD_ARRANGEMENT_101, 0, 0, 0x04, &charData) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, &connected) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardOpen(1, 0, 0, nullptr) == KEYBOARD_HANDLE); + CheckInvalidKeys(true); + CheckUnconvertedKeys(); + CheckKanaLed(); Require(sceKeyboardOpen(1, 0, 0, nullptr) == KEYBOARD_ERROR_ALREADY_OPENED); Require(sceKeyboardGetConnection(42, &connected) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, nullptr) == KEYBOARD_ERROR_INVALID_ARG); diff --git a/core/libs/prx/libSceLibcInternal/CMakeLists.txt b/core/libs/prx/libSceLibcInternal/CMakeLists.txt index 8087b230e5..f17760d179 100644 --- a/core/libs/prx/libSceLibcInternal/CMakeLists.txt +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -74,10 +74,11 @@ if(BUILD_TESTING) 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) + target_link_libraries(guest_posix_spawn_tests PRIVATE libSceLibcInternal libc libkernel) configure_windows_unwind(guest_posix_spawn_tests) add_test(NAME guest_posix_spawn COMMAND guest_posix_spawn_tests) set_tests_properties(guest_posix_spawn PROPERTIES TIMEOUT 20) + add_asan_test_options(guest_posix_spawn handle_sigbus=0) if(WIN32) set_tests_properties(guest_posix_spawn PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/prx/libSceLibcInternal/Directory.cpp b/core/libs/prx/libSceLibcInternal/Directory.cpp index ffb52e8614..3870c9cd6b 100644 --- a/core/libs/prx/libSceLibcInternal/Directory.cpp +++ b/core/libs/prx/libSceLibcInternal/Directory.cpp @@ -16,5 +16,8 @@ extern "C" void* APS5_VABI fdopendir_nid_postfix(int fd) { *__error_nid_postfix() = error == EBADF ? 9 : 20; return nullptr; } - return OpenDescriptorDirectory_nid_no_patch(path, fd, close_nid_postfix, GetDirectoryDescriptorPosition_nid_no_patch(fd)); + const bool virtualDirectory = IsDirectoryDescriptor_nid_no_patch(fd) != 0; + return OpenDescriptorDirectory_nid_no_patch(path, fd, close_nid_postfix, GetDirectoryDescriptorPosition_nid_no_patch(fd), + virtualDirectory ? ReadDirectoryDescriptorCallback_nid_no_patch : nullptr, + virtualDirectory ? SeekDirectoryDescriptorCallback_nid_no_patch : nullptr); } diff --git a/core/libs/prx/libSceLibcInternal/Spawn.cpp b/core/libs/prx/libSceLibcInternal/Spawn.cpp index 72e1bfcaae..4c7bd815b2 100644 --- a/core/libs/prx/libSceLibcInternal/Spawn.cpp +++ b/core/libs/prx/libSceLibcInternal/Spawn.cpp @@ -1,9 +1,16 @@ #include "prx/libc/include/Api.hpp" #include "prx/libSceLibcInternal/include/Api.hpp" #include "prx/libSceLibcInternal/include/Spawn.hpp" +#include "prx/libc/include/ChildProcesses.hpp" #include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/General.hpp" +#ifdef _WIN32 +#include "prx/libc/include/WindowsDescriptor.hpp" +#endif +#include "prx/libkernel/File/include/FileFlags.hpp" +#include #include +#include #include #include #include @@ -11,13 +18,42 @@ #include #include #include +#include +#ifdef _WIN32 +#include +#else +#include +#endif +#if defined(__linux__) +#include +#elif defined(__APPLE__) +#include +#include +#include +#endif + +#if defined(__linux__) +extern char** environ; +#endif namespace { constexpr int NoSuchFile = 2; constexpr int BadDescriptor = 9; constexpr int OutOfMemory = 12; +constexpr int Fault = 14; +constexpr int InvalidArgument = 22; constexpr std::string_view DefaultSearchPath = "/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"; +constexpr std::string_view ApplicationRoot = "/app0/"; +constexpr std::string_view MainExecutable = "/app0/eboot.bin"; +#ifdef _WIN32 +constexpr wchar_t LoadExecParentVariable[] = L"ANYPS5_LOADEXEC_PARENT"; +constexpr wchar_t LoadExecSkipVariable[] = L"ANYPS5_LOADEXEC_ARGV_SKIP="; +#else +constexpr std::uint32_t SpawnFlagsImplemented = 0x03; +#endif +constexpr std::uint32_t SpawnFlagResetIds = 0x01; +constexpr std::uint32_t SpawnFlagSetGroup = 0x02; PosixSpawnAttributes& Attributes(PosixSpawnAttributes* const* attributes, const char* function) { if (attributes == nullptr || *attributes == nullptr) throw std::invalid_argument(std::string(function) + ": uninitialized attributes"); @@ -29,36 +65,6 @@ PosixSpawnFileActions& FileActions(PosixSpawnFileActions* const* actions, const return **actions; } -bool GuestPathExists(const std::string& path) { - std::filesystem::path native; - if (ResolvePathError_nid_no_patch(path.c_str(), native) != 0) return false; - std::error_code error; - return std::filesystem::exists(native, error); -} - -bool ProgramFound(std::string_view name, bool searchesPath) { - if (name.empty()) return false; - if (!searchesPath || name.find('/') != std::string_view::npos) return GuestPathExists(std::string(name)); - const char* variable = getenv_nid_postfix("PATH"); - std::string_view directories = variable != nullptr ? std::string_view(variable) : DefaultSearchPath; - while (true) { - const auto separator = directories.find(':'); - const auto directory = directories.substr(0, separator); - if (GuestPathExists((directory.empty() ? std::string(".") : std::string(directory)) + '/' + std::string(name))) return true; - if (separator == std::string_view::npos) return false; - directories.remove_prefix(separator + 1); - } -} - -int Spawn(const char* function, const char* file, bool searchesPath, PosixSpawnFileActions* const* actions, PosixSpawnAttributes* const* attributes) { - if (file == nullptr) throw std::invalid_argument(std::string(function) + ": null path"); - if (actions != nullptr) FileActions(actions, function); - if (attributes != nullptr) Attributes(attributes, function); - if (!ProgramFound(file, searchesPath)) return NoSuchFile; - NotImplemented_nid_no_patch((std::string(function) + ": process creation").c_str()); - return -1; -} - int Append(PosixSpawnFileActions& actions, PosixSpawnFileAction action) { try { actions.actions.push_back(std::move(action)); @@ -68,6 +74,382 @@ int Append(PosixSpawnFileActions& actions, PosixSpawnFileAction action) { return 0; } +bool GuestPathExists(const std::string& path) { + std::filesystem::path native; + if (ResolvePathError_nid_no_patch(path.c_str(), native) != 0) return false; + std::error_code error; + return std::filesystem::exists(native, error); +} + +bool ProgramFound(std::string_view name, bool searchesPath, std::string& found) { + if (name.empty()) return false; + if (!searchesPath || name.find('/') != std::string_view::npos) { + found = std::string(name); + return GuestPathExists(found); + } + const char* variable = getenv_nid_postfix("PATH"); + std::string_view directories = variable != nullptr ? std::string_view(variable) : DefaultSearchPath; + while (true) { + const auto separator = directories.find(':'); + const auto directory = directories.substr(0, separator); + found = (directory.empty() ? std::string(".") : std::string(directory)) + '/' + std::string(name); + if (GuestPathExists(found)) return true; + if (separator == std::string_view::npos) return false; + directories.remove_prefix(separator + 1); + } +} + +std::string FlattenedExecutableName(const std::string& guest, const char* function) { + std::string_view relative(guest); + if (!relative.starts_with(ApplicationRoot)) NotImplemented_nid_no_patch((std::string(function) + ": executables outside /app0 (" + guest + ")").c_str()); + relative.remove_prefix(ApplicationRoot.size()); + std::string name; + while (true) { + const std::size_t separator = relative.find('/'); + const std::string_view component = relative.substr(0, separator); + if (component.empty() || component == "." || component == "..") NotImplemented_nid_no_patch((std::string(function) + ": a path with an empty, . or .. component (" + guest + ")").c_str()); + name += (name.empty() ? "" : ".") + std::string(component); + if (separator == std::string_view::npos) return name; + relative.remove_prefix(separator + 1); + } +} + +std::filesystem::path OwnExecutable() { +#if defined(_WIN32) + std::wstring own(32768, L'\0'); + const DWORD length = GetModuleFileNameW(nullptr, own.data(), static_cast(own.size())); + if (length == 0 || length >= own.size()) throw std::system_error(GetLastError(), std::system_category(), "posix_spawn: GetModuleFileNameW"); + own.resize(length); + return std::filesystem::path(own); +#elif defined(__APPLE__) + std::uint32_t size = 0; + _NSGetExecutablePath(nullptr, &size); + std::vector executable(size); + std::error_code error; + if (_NSGetExecutablePath(executable.data(), &size) == 0) return std::filesystem::canonical(executable.data(), error); + throw std::runtime_error("posix_spawn: cannot locate the executable"); +#else + std::error_code error; + const auto own = std::filesystem::read_symlink("/proc/self/exe", error); + if (error) throw std::system_error(error, "posix_spawn: cannot read /proc/self/exe"); + return own; +#endif +} + +std::filesystem::path ConvertedExecutableTarget(const char* function, const std::string& guest) { + const auto own = OwnExecutable(); + if (guest == MainExecutable) return own; + std::error_code error; + const auto name = std::filesystem::path(reinterpret_cast((FlattenedExecutableName(guest, function) + own.extension().string()).c_str())); + const auto target = own.parent_path() / name; + if (!std::filesystem::is_regular_file(target, error)) + NotImplemented_nid_no_patch((std::string(function) + ": " + guest + " without its converted executable; relink with --executable " + std::string(guest.substr(ApplicationRoot.size()))).c_str()); + return target; +} + +int HostOpenFlags(std::int32_t sceFlags) { + int f = 0; + const int acc = sceFlags & SCE_KERNEL_O_ACCMODE; + if (acc == SCE_KERNEL_O_RDONLY) f |= 0; + else if (acc == SCE_KERNEL_O_WRONLY) f |= 1; + else if (acc == SCE_KERNEL_O_RDWR) f |= 2; + else throw std::invalid_argument("posix_spawn_file_actions_addopen: invalid access mode"); +#ifdef _WIN32 + if (sceFlags & SCE_KERNEL_O_APPEND) f |= _O_APPEND; + if (sceFlags & SCE_KERNEL_O_CREAT) f |= _O_CREAT; + if (sceFlags & SCE_KERNEL_O_TRUNC) f |= _O_TRUNC; + if (sceFlags & SCE_KERNEL_O_EXCL) f |= _O_EXCL; + f |= _O_BINARY; +#else + if (sceFlags & SCE_KERNEL_O_APPEND) f |= O_APPEND; + if (sceFlags & SCE_KERNEL_O_CREAT) f |= O_CREAT; + if (sceFlags & SCE_KERNEL_O_TRUNC) f |= O_TRUNC; + if (sceFlags & SCE_KERNEL_O_EXCL) f |= O_EXCL; +#endif + return f; +} + +#ifdef _WIN32 +std::wstring Utf16(const std::string& text) { + if (text.empty()) return {}; + const int size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast(text.size()), nullptr, 0); + if (size <= 0) throw std::runtime_error("posix_spawn: argument is not valid UTF-8"); + std::wstring wide(static_cast(size), L'\0'); + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast(text.size()), wide.data(), size); + return wide; +} + +std::wstring QuotedArgument(const std::wstring& argument) { + if (!argument.empty() && argument.find_first_of(L" \t\n\v\"") == std::wstring::npos) return argument; + std::wstring quoted = L"\""; + for (std::size_t index = 0;; ++index) { + std::size_t backslashes = 0; + while (index < argument.size() && argument[index] == L'\\') { + ++backslashes; + ++index; + } + if (index == argument.size()) { + quoted.append(backslashes * 2, L'\\'); + break; + } + quoted.append(argument[index] == L'"' ? backslashes * 2 + 1 : backslashes, L'\\'); + quoted.push_back(argument[index]); + } + quoted.push_back(L'"'); + return quoted; +} +#endif + +std::string HexFlag(std::uint32_t value) { + constexpr char digits[] = "0123456789abcdef"; + std::string text; + if (value == 0) text.push_back('0'); + while (value > 0) { + text.insert(text.begin(), digits[value & 0xfu]); + value >>= 4; + } + return "0x" + text; +} + +std::uint32_t SpawnAttributeFlags(PosixSpawnAttributes* const* attributes) { + return attributes != nullptr && *attributes != nullptr ? static_cast(static_cast((*attributes)->flags)) : 0; +} + +#ifdef _WIN32 +struct ChildStreams { + HANDLE handles[3] = {INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE}; +}; + +HANDLE DuplicateInheritable(HANDLE source) { + HANDLE handle = nullptr; + if (!DuplicateHandle(GetCurrentProcess(), source, GetCurrentProcess(), &handle, 0, TRUE, DUPLICATE_SAME_ACCESS)) + throw std::system_error(GetLastError(), std::system_category(), "posix_spawn: duplicating a descriptor"); + return handle; +} + +void CloseRedirectedStreams(ChildStreams& streams) { + for (HANDLE handle : streams.handles) + if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle); +} + +void ReplaceChildStream(ChildStreams& streams, int descriptor, HANDLE source) { + HANDLE previous = streams.handles[descriptor]; + streams.handles[descriptor] = source == INVALID_HANDLE_VALUE ? INVALID_HANDLE_VALUE : DuplicateInheritable(source); + if (previous != INVALID_HANDLE_VALUE) CloseHandle(previous); +} + +int ApplyChildRedirection(ChildStreams& streams, const PosixSpawnFileAction& action) { + switch (action.kind) { + case PosixSpawnFileActionKind::Close: + if (action.descriptor >= 0 && action.descriptor < 3) ReplaceChildStream(streams, action.descriptor, INVALID_HANDLE_VALUE); + return 0; + case PosixSpawnFileActionKind::Duplicate: + if (action.descriptor < 0 || action.newDescriptor < 0) return BadDescriptor; + if (action.newDescriptor > 2) + NotImplemented_nid_no_patch(("posix_spawn: a child descriptor " + std::to_string(action.newDescriptor) + " beyond the standard streams").c_str()); + { + const HANDLE source = WindowsDescriptorHandle_nid_no_patch(action.descriptor); + if (source == INVALID_HANDLE_VALUE) return BadDescriptor; + ReplaceChildStream(streams, action.newDescriptor, source); + } + return 0; + case PosixSpawnFileActionKind::Open: { + if (action.descriptor < 0) return BadDescriptor; + if (action.descriptor > 2) + NotImplemented_nid_no_patch(("posix_spawn: a child descriptor " + std::to_string(action.descriptor) + " beyond the standard streams").c_str()); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(action.path.c_str(), native)) return error; + try { + const int fd = ::_wopen(native.c_str(), HostOpenFlags(action.openFlags), action.mode); + if (fd < 0) return errno; + const HANDLE source = WindowsDescriptorHandle_nid_no_patch(fd); + ReplaceChildStream(streams, action.descriptor, source); + ::_close(fd); + } catch (const std::invalid_argument&) { + return InvalidArgument; + } + return 0; + } + } + return 0; +} + +std::vector ChildEnvironment(char* const* environment) { + std::vector entries; + const auto missesMarkers = [](const std::wstring& entry) { + return entry.compare(0, std::size(LoadExecParentVariable) - 1, LoadExecParentVariable) != 0 && + entry.compare(0, std::size(LoadExecSkipVariable) - 1, LoadExecSkipVariable) != 0; + }; + if (environment != nullptr) + for (std::size_t index = 0; environment[index] != nullptr; ++index) { + const std::wstring entry = Utf16(environment[index]); + if (missesMarkers(entry)) + entries.push_back(entry); + } + else { + const LPWCH block = GetEnvironmentStringsW(); + for (const wchar_t* text = block; *text != L'\0'; ) { + std::size_t length = 0; + while (text[length] != L'\0') ++length; + if (length > 0) { + const std::wstring entry(text, length); + if (missesMarkers(entry)) + entries.push_back(entry); + } + text += length + 1; + } + FreeEnvironmentStringsW(block); + } + entries.push_back(std::wstring(LoadExecSkipVariable) + L"1"); + std::sort(entries.begin(), entries.end(), [](const auto& left, const auto& right) { return _wcsicmp(left.c_str(), right.c_str()) < 0; }); + std::vector block; + for (const auto& entry : entries) { + block.insert(block.end(), entry.begin(), entry.end()); + block.push_back(0); + } + block.push_back(0); + return block; +} + +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); + if (const std::uint32_t attributes_flags = SpawnAttributeFlags(attributes); attributes_flags != 0) + NotImplemented_nid_no_patch(("posix_spawn: the attribute flags " + HexFlag(attributes_flags) + " on this host").c_str()); + ChildStreams streams; + const DWORD identifiers[] = {STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE}; + bool redirected = false; + for (std::size_t index = 0; index < 3; ++index) { + const HANDLE source = GetStdHandle(identifiers[index]); + if (source == nullptr || source == INVALID_HANDLE_VALUE) continue; + streams.handles[index] = DuplicateInheritable(source); + redirected = true; + } + if (actions != nullptr) { + for (const auto& action : (*actions)->actions) { + if (action.kind != PosixSpawnFileActionKind::Close || (action.descriptor >= 0 && action.descriptor < 3)) redirected = true; + const int error = ApplyChildRedirection(streams, action); + if (error != 0) { + CloseRedirectedStreams(streams); + return error; + } + } + } + std::wstring command = L"\"" + target.wstring() + L"\""; + for (std::size_t index = 0; arguments[index] != nullptr; ++index) + command += L" " + QuotedArgument(Utf16(arguments[index])); + if (command.size() >= 32767) throw std::runtime_error("posix_spawn: command line is too long"); + auto environmentBlock = ChildEnvironment(environment); + PROCESS_INFORMATION process{}; + const BOOL created = [&] { + if (!redirected || (streams.handles[0] == INVALID_HANDLE_VALUE && streams.handles[1] == INVALID_HANDLE_VALUE && streams.handles[2] == INVALID_HANDLE_VALUE)) { + STARTUPINFOW plain{}; + plain.cb = sizeof(plain); + return CreateProcessW(const_cast(target.c_str()), command.data(), nullptr, nullptr, FALSE, + environmentBlock.empty() ? 0u : CREATE_UNICODE_ENVIRONMENT, environmentBlock.empty() ? nullptr : environmentBlock.data(), + nullptr, &plain, &process); + } + STARTUPINFOEXW startup{}; + startup.StartupInfo.cb = sizeof(startup); + startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES; + startup.StartupInfo.hStdInput = streams.handles[0]; + startup.StartupInfo.hStdOutput = streams.handles[1]; + startup.StartupInfo.hStdError = streams.handles[2]; + std::vector inherited; + for (HANDLE handle : streams.handles) + if (handle != INVALID_HANDLE_VALUE) inherited.push_back(handle); + SIZE_T bytes = 0; + InitializeProcThreadAttributeList(nullptr, 1, 0, &bytes); + std::vector storage((bytes + sizeof(std::max_align_t) - 1) / sizeof(std::max_align_t)); + auto* list = reinterpret_cast(storage.data()); + if (!InitializeProcThreadAttributeList(list, 1, 0, &bytes) || + !UpdateProcThreadAttribute(list, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, inherited.data(), inherited.size() * sizeof(HANDLE), nullptr, nullptr)) + throw std::system_error(GetLastError(), std::system_category(), "posix_spawn: selecting inherited handles"); + const std::unique_ptr<_PROC_THREAD_ATTRIBUTE_LIST, void (*)(_PROC_THREAD_ATTRIBUTE_LIST*)> cleanup(list, DeleteProcThreadAttributeList); + return CreateProcessW(const_cast(target.c_str()), command.data(), nullptr, nullptr, TRUE, + EXTENDED_STARTUPINFO_PRESENT | (environmentBlock.empty() ? 0u : CREATE_UNICODE_ENVIRONMENT), + environmentBlock.empty() ? nullptr : environmentBlock.data(), nullptr, &startup.StartupInfo, &process); + }(); + if (!created) { + const DWORD error = GetLastError(); + CloseRedirectedStreams(streams); + throw std::system_error(error, std::system_category(), "posix_spawn: creating the converted executable"); + } + CloseRedirectedStreams(streams); + CloseHandle(process.hThread); + RegisterChildProcess_nid_no_patch(process.dwProcessId, static_cast(reinterpret_cast(process.hProcess))); + *processId = static_cast(process.dwProcessId); + return 0; +} +#else +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); + posix_spawnattr_t hostAttributes; + posix_spawnattr_t* usedAttributes = nullptr; + const std::uint32_t flags = SpawnAttributeFlags(attributes); + 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; + usedAttributes = &hostAttributes; + if ((flags & SpawnFlagResetIds) != 0) posix_spawnattr_setflags(usedAttributes, POSIX_SPAWN_RESETIDS); + if ((flags & SpawnFlagSetGroup) != 0) { + posix_spawnattr_setflags(usedAttributes, POSIX_SPAWN_SETPGROUP); + posix_spawnattr_setpgroup(usedAttributes, (*attributes)->processGroup); + } + } + 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 (usedAttributes != nullptr) posix_spawnattr_destroy(usedAttributes); + return Fault; + } + usedActions = &hostActions; + } + int result = 0; + if (actions != nullptr && usedActions != nullptr) + for (const auto& action : (*actions)->actions) { + switch (action.kind) { + case PosixSpawnFileActionKind::Close: + result = posix_spawn_file_actions_addclose(usedActions, action.descriptor); + break; + case PosixSpawnFileActionKind::Duplicate: + result = 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); + } catch (const std::invalid_argument&) { + result = InvalidArgument; + } + } + break; + } + } + if (result != 0) break; + } + if (result == 0) { + char* const* hostEnvironment = environment; +#if defined(__linux__) + if (hostEnvironment == nullptr) hostEnvironment = environ; +#elif defined(__APPLE__) + if (hostEnvironment == nullptr) hostEnvironment = *_NSGetEnviron(); +#endif + pid_t childPid = -1; + result = posix_spawn(&childPid, target.c_str(), usedActions, usedAttributes, arguments, hostEnvironment); + if (result == 0) *processId = static_cast(childPid); + } + if (usedActions != nullptr) posix_spawn_file_actions_destroy(usedActions); + if (usedAttributes != nullptr) posix_spawnattr_destroy(usedAttributes); + return result; +} +#endif + } extern "C" { @@ -198,18 +580,23 @@ int APS5_VABI posix_spawn_file_actions_addclose_nid_postfix(PosixSpawnFileAction return Append(FileActions(actions, "posix_spawn_file_actions_addclose"), {PosixSpawnFileActionKind::Close, descriptor, {}, 0, 0, 0}); } +int SpawnProgram(const char* function, const char* file, bool searchesPath, PosixSpawnFileActions* const* actions, + PosixSpawnAttributes* const* attributes, std::int32_t* processId, char* const* arguments, char* const* environment) { + if (file == nullptr) throw std::invalid_argument(std::string(function) + ": null path"); + if (actions != nullptr) FileActions(actions, function); + if (attributes != nullptr) Attributes(attributes, function); + std::string found; + if (!ProgramFound(file, searchesPath, found)) return NoSuchFile; + if (arguments == nullptr) return Fault; + return SpawnChild(function, found, actions, attributes, processId, 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) { - (void)processId; - (void)arguments; - (void)environment; - return Spawn("posix_spawn", path, false, actions, attributes); + return SpawnProgram("posix_spawn", path, false, actions, attributes, processId, arguments, environment); } int APS5_VABI posix_spawnp_nid_postfix(std::int32_t* processId, const char* file, PosixSpawnFileActions* const* actions, PosixSpawnAttributes* const* attributes, char* const* arguments, char* const* environment) { - (void)processId; - (void)arguments; - (void)environment; - return Spawn("posix_spawnp", file, true, actions, attributes); + return SpawnProgram("posix_spawnp", file, true, actions, attributes, processId, arguments, environment); } } diff --git a/core/libs/prx/libSceLibreSsl/Alert.cpp b/core/libs/prx/libSceLibreSsl/Alert.cpp new file mode 100644 index 0000000000..f0783dd6b0 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/Alert.cpp @@ -0,0 +1,45 @@ +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" + +extern "C" const char* APS5_VABI SSL_alert_desc_string_long_nid_postfix(int value) { + struct Description { + unsigned int code; + const char* text; + }; + static constexpr Description descriptions[] = { + {0, "close notify"}, + {10, "unexpected_message"}, + {20, "bad record mac"}, + {21, "decryption failed"}, + {22, "record overflow"}, + {30, "decompression failure"}, + {40, "handshake failure"}, + {41, "no certificate"}, + {42, "bad certificate"}, + {43, "unsupported certificate"}, + {44, "certificate revoked"}, + {45, "certificate expired"}, + {46, "certificate unknown"}, + {47, "illegal parameter"}, + {48, "unknown CA"}, + {49, "access denied"}, + {50, "decode error"}, + {51, "decrypt error"}, + {60, "export restriction"}, + {70, "protocol version"}, + {71, "insufficient security"}, + {80, "internal error"}, + {90, "user canceled"}, + {100, "no renegotiation"}, + {110, "unsupported extension"}, + {111, "certificate unobtainable"}, + {112, "unrecognized name"}, + {113, "bad certificate status response"}, + {114, "bad certificate hash value"}, + {115, "unknown PSK identity"}, + }; + const auto code = static_cast(value) & 0xffu; + for (const auto& description : descriptions) { + if (description.code == code) return description.text; + } + return "unknown"; +} diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt index ea084db6e0..91af95cff4 100644 --- a/core/libs/prx/libSceLibreSsl/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -1,7 +1,8 @@ -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) +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) 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) if(BUILD_TESTING) add_test_executable(guest_libressl_x509_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslX509Errors.cpp) @@ -21,8 +22,15 @@ if(BUILD_TESTING) configure_windows_unwind(guest_libressl_digest_tests) add_test(NAME guest_libressl_digest COMMAND guest_libressl_digest_tests) set_tests_properties(guest_libressl_digest PROPERTIES TIMEOUT 30) + add_test_executable(guest_libressl_asn1_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Asn1.cpp) + target_include_directories(guest_libressl_asn1_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_asn1_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_asn1_tests) + add_test(NAME guest_libressl_asn1 COMMAND guest_libressl_asn1_tests) + add_test(NAME guest_libressl_asn1_allocator COMMAND guest_libressl_asn1_tests allocator) + set_tests_properties(guest_libressl_asn1 guest_libressl_asn1_allocator PROPERTIES TIMEOUT 30) if(WIN32) - set_tests_properties(guest_libressl_digest PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_libressl_digest guest_libressl_asn1 guest_libressl_asn1_allocator PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() @@ -58,4 +66,104 @@ if(BUILD_TESTING) set_tests_properties(guest_libressl_aes PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_libressl_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslError.cpp) + target_include_directories(guest_libressl_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_error_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_error_tests) + add_test(NAME guest_libressl_error COMMAND guest_libressl_error_tests) + set_tests_properties(guest_libressl_error PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_error PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_libressl_ocsp_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslOcsp.cpp) + target_include_directories(guest_libressl_ocsp_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_ocsp_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_ocsp_tests) + add_test(NAME guest_libressl_ocsp COMMAND guest_libressl_ocsp_tests) + set_tests_properties(guest_libressl_ocsp PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_ocsp PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_libressl_stack_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslStack.cpp) + target_include_directories(guest_libressl_stack_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_stack_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_stack_tests) + add_test(NAME guest_libressl_stack COMMAND guest_libressl_stack_tests) + set_tests_properties(guest_libressl_stack PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_stack PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_libressl_ui_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslUi.cpp) + target_include_directories(guest_libressl_ui_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_ui_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_ui_tests) + add_test(NAME guest_libressl_ui COMMAND guest_libressl_ui_tests) + set_tests_properties(guest_libressl_ui PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_ui PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Cipher.cpp) + +if(BUILD_TESTING) + add_test_executable(guest_libressl_cipher_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslCipher.cpp) + target_include_directories(guest_libressl_cipher_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_cipher_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_cipher_tests) + add_test(NAME guest_libressl_cipher COMMAND guest_libressl_cipher_tests) + set_tests_properties(guest_libressl_cipher PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_cipher PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/Alert.cpp) + +if(BUILD_TESTING) + add_test_executable(guest_libressl_alert_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslAlert.cpp) + target_include_directories(guest_libressl_alert_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_alert_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_alert_tests) + add_test(NAME guest_libressl_alert COMMAND guest_libressl_alert_tests) + set_tests_properties(guest_libressl_alert PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_alert PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +if(BUILD_TESTING) + add_test_executable(guest_libressl_version_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslVersion.cpp) + target_include_directories(guest_libressl_version_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_version_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_version_tests) + add_test(NAME guest_libressl_version COMMAND guest_libressl_version_tests) + set_tests_properties(guest_libressl_version PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_version PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +if(BUILD_TESTING) + add_test_executable(guest_libressl_crypto_locks_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslCryptoLocks.cpp) + target_include_directories(guest_libressl_crypto_locks_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_crypto_locks_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_crypto_locks_tests) + add_test(NAME guest_libressl_crypto_locks COMMAND guest_libressl_crypto_locks_tests) + set_tests_properties(guest_libressl_crypto_locks PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_libressl_crypto_locks PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 84be6cef36..bc950b9224 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -1,4 +1,5 @@ #include "prx/libSceLibreSsl/include/Api.hpp" +#include #include #include #include @@ -13,42 +14,24 @@ namespace { constexpr std::size_t DesScheduleBytes = 128; +constexpr int CryptoNumLocks = 41; + +static_assert(sizeof(CryptoThreadId) == 16); + +std::atomic lockingCallback{nullptr}; +std::atomic addLockCallback{nullptr}; +std::atomic threadIdCallback{nullptr}; } extern "C" { APS5_DUMMY_FUN -int APS5_VABI ASN1_INTEGER_get_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ASN1_STRING_data_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ASN1_STRING_length_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI ASN1_STRING_print_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI ASN1_STRING_to_UTF8_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ASN1_STRING_type_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI ASN1_TIME_print_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -229,14 +212,15 @@ int APS5_VABI CONF_modules_load_file_nid_postfix(void) { return 0; } -int APS5_VABI CRYPTO_THREADID_set_callback_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI CRYPTO_THREADID_set_callback_nid_postfix(void* callback) { + void* expected = nullptr; + return threadIdCallback.compare_exchange_strong(expected, callback) ? 1 : 0; } -int APS5_VABI CRYPTO_THREADID_set_numeric_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI CRYPTO_THREADID_set_numeric_nid_postfix(CryptoThreadId* id, std::uint64_t value) { + if (id == nullptr) APS5_INVALID_ARG_EX; + *id = {}; + id->val = value; } int APS5_VABI CRYPTO_cleanup_all_ex_data_nid_postfix(void) { @@ -245,18 +229,15 @@ int APS5_VABI CRYPTO_cleanup_all_ex_data_nid_postfix(void) { } int APS5_VABI CRYPTO_num_locks_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return CryptoNumLocks; } -int APS5_VABI CRYPTO_set_add_lock_callback_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI CRYPTO_set_add_lock_callback_nid_postfix(void* callback) { + addLockCallback.store(callback); } -int APS5_VABI CRYPTO_set_locking_callback_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI CRYPTO_set_locking_callback_nid_postfix(void* callback) { + lockingCallback.store(callback); } void APS5_VABI DES_ecb_encrypt_nid_postfix(const unsigned char* input, unsigned char* output, const unsigned char* schedule, int enc) { @@ -473,11 +454,6 @@ int APS5_VABI ENGINE_set_default_nid_postfix(void) { return 0; } -int APS5_VABI ERR_clear_error_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI ERR_error_string_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -493,21 +469,6 @@ int APS5_VABI ERR_free_strings_nid_postfix(void) { return 0; } -int APS5_VABI ERR_get_error_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ERR_peek_error_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ERR_remove_thread_state_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI EVP_CIPHER_CTX_cleanup_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -858,21 +819,11 @@ int APS5_VABI OCSP_basic_verify_nid_postfix(void) { return 0; } -int APS5_VABI OCSP_cert_status_str_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI OCSP_check_validity_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI OCSP_crl_reason_str_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI OCSP_resp_count_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -893,11 +844,6 @@ int APS5_VABI OCSP_response_status_nid_postfix(void) { return 0; } -int APS5_VABI OCSP_response_status_str_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI OCSP_single_get0_status_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1028,21 +974,6 @@ int APS5_VABI RSA_verify_PKCS1_PSS_nid_postfix(void) { return 0; } -int APS5_VABI SSL_CIPHER_get_bits_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SSL_CIPHER_get_name_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SSL_CIPHER_get_version_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI SSL_CTX_add_client_CA_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1233,11 +1164,6 @@ int APS5_VABI SSL_accept_nid_postfix(void) { return 0; } -int APS5_VABI SSL_alert_desc_string_long_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI SSL_check_private_key_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1458,9 +1384,8 @@ int APS5_VABI SSL_write_nid_postfix(void) { return 0; } -int APS5_VABI SSLeay_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI SSLeay_nid_postfix(void) { + return 0x20000000; } int APS5_VABI SSLv23_client_method_nid_postfix(void) { @@ -1483,16 +1408,6 @@ int APS5_VABI UI_OpenSSL_nid_postfix(void) { return 0; } -int APS5_VABI UI_create_method_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI UI_destroy_method_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI UI_get0_user_data_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1508,46 +1423,6 @@ int APS5_VABI UI_get_string_type_nid_postfix(void) { return 0; } -int APS5_VABI UI_method_get_closer_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI UI_method_get_opener_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI UI_method_get_reader_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI UI_method_get_writer_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI UI_method_set_closer_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI UI_method_set_opener_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI UI_method_set_reader_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI UI_method_set_writer_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI UI_set_result_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1873,24 +1748,4 @@ int APS5_VABI sceLibreSslTerm(void) { return 0; } -int APS5_VABI sk_num_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sk_pop_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sk_pop_free_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sk_value_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceLibreSsl/include/Api.hpp b/core/libs/prx/libSceLibreSsl/include/Api.hpp index cf2b0a75e1..02d0223f0e 100644 --- a/core/libs/prx/libSceLibreSsl/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl/include/Api.hpp @@ -2,6 +2,13 @@ #include "SceTypes.hpp" +struct Asn1String { + std::int32_t length; + std::int32_t type; + unsigned char* data; + std::int64_t flags; +}; + inline constexpr int MaxRounds = 14; struct AesKey { @@ -9,6 +16,11 @@ struct AesKey { std::int32_t rounds; }; +struct CryptoThreadId { + void* ptr; + std::uint64_t val; +}; + struct Md4Context { std::uint32_t A, B, C, D; std::uint32_t Nl, Nh; @@ -44,14 +56,39 @@ struct Sha512Context { std::uint32_t num, mdLength; }; +using StackCompare = int (APS5_VABI*)(const void*, const void*); +using StackElementFree = void (APS5_VABI*)(void*); + +struct LibreSslStack { + int num; + char** data; + int sorted; + int numAlloc; + StackCompare comp; +}; + +using UiSessionFunction = int (APS5_VABI*)(void*); +using UiStringFunction = int (APS5_VABI*)(void*, void*); +using UiPromptFunction = char* (APS5_VABI*)(void*, const char*, const char*); + +struct LibreSslUiMethod { + char* name; + UiSessionFunction openSession; + UiStringFunction writeString; + UiSessionFunction flush; + UiStringFunction readString; + UiSessionFunction closeSession; + UiPromptFunction constructPrompt; +}; + extern "C" { -int APS5_VABI ASN1_INTEGER_get_nid_postfix(void); -int APS5_VABI ASN1_STRING_data_nid_postfix(void); -int APS5_VABI ASN1_STRING_length_nid_postfix(void); +std::int64_t APS5_VABI ASN1_INTEGER_get_nid_postfix(const Asn1String* value); +unsigned char* APS5_VABI ASN1_STRING_data_nid_postfix(Asn1String* value); +int APS5_VABI ASN1_STRING_length_nid_postfix(const Asn1String* value); int APS5_VABI ASN1_STRING_print_nid_postfix(void); -int APS5_VABI ASN1_STRING_to_UTF8_nid_postfix(void); -int APS5_VABI ASN1_STRING_type_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); int APS5_VABI BIO_f_ssl_nid_postfix(void); @@ -88,12 +125,12 @@ int APS5_VABI CMAC_Init_nid_postfix(void); int APS5_VABI CMAC_Update_nid_postfix(void); 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); -int APS5_VABI CRYPTO_THREADID_set_numeric_nid_postfix(void); +int APS5_VABI CRYPTO_THREADID_set_callback_nid_postfix(void* callback); +void APS5_VABI CRYPTO_THREADID_set_numeric_nid_postfix(CryptoThreadId* id, std::uint64_t value); int APS5_VABI CRYPTO_cleanup_all_ex_data_nid_postfix(void); int APS5_VABI CRYPTO_num_locks_nid_postfix(void); -int APS5_VABI CRYPTO_set_add_lock_callback_nid_postfix(void); -int APS5_VABI CRYPTO_set_locking_callback_nid_postfix(void); +void APS5_VABI CRYPTO_set_add_lock_callback_nid_postfix(void* callback); +void APS5_VABI CRYPTO_set_locking_callback_nid_postfix(void* callback); void APS5_VABI DES_ecb_encrypt_nid_postfix(const unsigned char* input, unsigned char* output, const unsigned char* schedule, int enc); int APS5_VABI DES_set_key_nid_postfix(const unsigned char* key, unsigned char* schedule); void APS5_VABI DES_set_odd_parity_nid_postfix(unsigned char* key); @@ -135,13 +172,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); -int APS5_VABI ERR_clear_error_nid_postfix(void); +void APS5_VABI ERR_clear_error_nid_postfix(void); int APS5_VABI ERR_error_string_nid_postfix(void); int APS5_VABI ERR_error_string_n_nid_postfix(void); int APS5_VABI ERR_free_strings_nid_postfix(void); -int APS5_VABI ERR_get_error_nid_postfix(void); -int APS5_VABI ERR_peek_error_nid_postfix(void); -int APS5_VABI ERR_remove_thread_state_nid_postfix(void); +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); 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); @@ -212,14 +249,14 @@ int APS5_VABI OCSP_BASICRESP_free_nid_postfix(void); int APS5_VABI OCSP_RESPONSE_free_nid_postfix(void); int APS5_VABI OCSP_basic_add1_cert_nid_postfix(void); int APS5_VABI OCSP_basic_verify_nid_postfix(void); -int APS5_VABI OCSP_cert_status_str_nid_postfix(void); +const char* APS5_VABI OCSP_cert_status_str_nid_postfix(std::int64_t status); int APS5_VABI OCSP_check_validity_nid_postfix(void); -int APS5_VABI OCSP_crl_reason_str_nid_postfix(void); +const char* APS5_VABI OCSP_crl_reason_str_nid_postfix(std::int64_t reason); int APS5_VABI OCSP_resp_count_nid_postfix(void); int APS5_VABI OCSP_resp_get0_nid_postfix(void); int APS5_VABI OCSP_response_get1_basic_nid_postfix(void); int APS5_VABI OCSP_response_status_nid_postfix(void); -int APS5_VABI OCSP_response_status_str_nid_postfix(void); +const char* APS5_VABI OCSP_response_status_str_nid_postfix(std::int64_t status); int APS5_VABI OCSP_single_get0_status_nid_postfix(void); int APS5_VABI OPENSSL_add_all_algorithms_noconf_nid_postfix(void); int APS5_VABI OPENSSL_load_builtin_modules_nid_postfix(void); @@ -246,9 +283,9 @@ int APS5_VABI RSA_sign_nid_postfix(void); int APS5_VABI RSA_size_nid_postfix(void); int APS5_VABI RSA_verify_nid_postfix(void); int APS5_VABI RSA_verify_PKCS1_PSS_nid_postfix(void); -int APS5_VABI SSL_CIPHER_get_bits_nid_postfix(void); -int APS5_VABI SSL_CIPHER_get_name_nid_postfix(void); -int APS5_VABI SSL_CIPHER_get_version_nid_postfix(void); +int APS5_VABI SSL_CIPHER_get_bits_nid_postfix(const void* cipher, std::int32_t* algorithmBits); +const char* APS5_VABI SSL_CIPHER_get_name_nid_postfix(const void* cipher); +char* APS5_VABI SSL_CIPHER_get_version_nid_postfix(const void* cipher); int APS5_VABI SSL_CTX_add_client_CA_nid_postfix(void); int APS5_VABI SSL_CTX_callback_ctrl_nid_postfix(void); int APS5_VABI SSL_CTX_check_private_key_nid_postfix(void); @@ -287,7 +324,7 @@ int APS5_VABI SSL_CTX_use_certificate_chain_file_nid_postfix(void); int APS5_VABI SSL_CTX_use_certificate_file_nid_postfix(void); int APS5_VABI SSL_SESSION_free_nid_postfix(void); int APS5_VABI SSL_accept_nid_postfix(void); -int APS5_VABI SSL_alert_desc_string_long_nid_postfix(void); +const char* APS5_VABI SSL_alert_desc_string_long_nid_postfix(int value); int APS5_VABI SSL_check_private_key_nid_postfix(void); int APS5_VABI SSL_connect_nid_postfix(void); int APS5_VABI SSL_ctrl_nid_postfix(void); @@ -332,24 +369,24 @@ int APS5_VABI SSL_use_certificate_ASN1_nid_postfix(void); int APS5_VABI SSL_version_nid_postfix(void); int APS5_VABI SSL_want_nid_postfix(void); int APS5_VABI SSL_write_nid_postfix(void); -int APS5_VABI SSLeay_nid_postfix(void); +std::uint64_t APS5_VABI SSLeay_nid_postfix(void); int APS5_VABI SSLv23_client_method_nid_postfix(void); int APS5_VABI SSLv23_server_method_nid_postfix(void); int APS5_VABI TLS_client_method_nid_postfix(void); int APS5_VABI UI_OpenSSL_nid_postfix(void); -int APS5_VABI UI_create_method_nid_postfix(void); -int APS5_VABI UI_destroy_method_nid_postfix(void); +LibreSslUiMethod* APS5_VABI UI_create_method_nid_postfix(char* name); +void APS5_VABI UI_destroy_method_nid_postfix(LibreSslUiMethod* method); int APS5_VABI UI_get0_user_data_nid_postfix(void); int APS5_VABI UI_get_input_flags_nid_postfix(void); int APS5_VABI UI_get_string_type_nid_postfix(void); -int APS5_VABI UI_method_get_closer_nid_postfix(void); -int APS5_VABI UI_method_get_opener_nid_postfix(void); -int APS5_VABI UI_method_get_reader_nid_postfix(void); -int APS5_VABI UI_method_get_writer_nid_postfix(void); -int APS5_VABI UI_method_set_closer_nid_postfix(void); -int APS5_VABI UI_method_set_opener_nid_postfix(void); -int APS5_VABI UI_method_set_reader_nid_postfix(void); -int APS5_VABI UI_method_set_writer_nid_postfix(void); +UiSessionFunction APS5_VABI UI_method_get_closer_nid_postfix(const LibreSslUiMethod* method); +UiSessionFunction APS5_VABI UI_method_get_opener_nid_postfix(const LibreSslUiMethod* method); +UiStringFunction APS5_VABI UI_method_get_reader_nid_postfix(const LibreSslUiMethod* method); +UiStringFunction APS5_VABI UI_method_get_writer_nid_postfix(const LibreSslUiMethod* method); +int APS5_VABI UI_method_set_closer_nid_postfix(LibreSslUiMethod* method, UiSessionFunction closer); +int APS5_VABI UI_method_set_opener_nid_postfix(LibreSslUiMethod* method, UiSessionFunction opener); +int APS5_VABI UI_method_set_reader_nid_postfix(LibreSslUiMethod* method, UiStringFunction reader); +int APS5_VABI UI_method_set_writer_nid_postfix(LibreSslUiMethod* method, UiStringFunction writer); int APS5_VABI UI_set_result_nid_postfix(void); int APS5_VABI X509V3_EXT_print_nid_postfix(void); int APS5_VABI X509_EXTENSION_get_critical_nid_postfix(void); @@ -403,10 +440,10 @@ int APS5_VABI sceLibreSslInit(void); int APS5_VABI sceLibreSslInitEx(void); int APS5_VABI sceLibreSslInitEx2(void); int APS5_VABI sceLibreSslTerm(void); -int APS5_VABI sk_num_nid_postfix(void); -int APS5_VABI sk_pop_nid_postfix(void); -int APS5_VABI sk_pop_free_nid_postfix(void); -int APS5_VABI sk_value_nid_postfix(void); +int APS5_VABI sk_num_nid_postfix(const LibreSslStack* stack); +void* APS5_VABI sk_pop_nid_postfix(LibreSslStack* stack); +void APS5_VABI sk_pop_free_nid_postfix(LibreSslStack* stack, StackElementFree release); +void* APS5_VABI sk_value_nid_postfix(const LibreSslStack* stack, int index); int APS5_VABI AES_set_encrypt_key_nid_postfix(const unsigned char* userKey, int bits, AesKey* key); int APS5_VABI AES_set_decrypt_key_nid_postfix(const unsigned char* userKey, int bits, AesKey* key); void APS5_VABI AES_encrypt_nid_postfix(const unsigned char* in, unsigned char* out, const AesKey* key); diff --git a/core/libs/prx/libSceLibreSsl/include/Error.hpp b/core/libs/prx/libSceLibreSsl/include/Error.hpp new file mode 100644 index 0000000000..0c7a23712f --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/include/Error.hpp @@ -0,0 +1,10 @@ +#ifndef CORE_LIBS_PRX_LIBSCELIBRESSL_INCLUDE_ERROR_HPP +#define CORE_LIBS_PRX_LIBSCELIBRESSL_INCLUDE_ERROR_HPP + +extern "C" { + +void LibreSslPutError_nid_no_patch(int library, int function, int reason); + +} + +#endif diff --git a/core/libs/prx/libSceLibreSsl/src/Aes.cpp b/core/libs/prx/libSceLibreSsl/src/Aes.cpp index 4c908d646c..ddbdbe61db 100644 --- a/core/libs/prx/libSceLibreSsl/src/Aes.cpp +++ b/core/libs/prx/libSceLibreSsl/src/Aes.cpp @@ -27,9 +27,10 @@ constexpr std::uint8_t Multiply(std::uint8_t a, std::uint8_t b) { constexpr std::array MakeSubstitution() { std::array box {}; for (int value = 0; value < 256; ++value) { - std::uint8_t inverse = 0; - for (int candidate = 1; candidate < 256 && value; ++candidate) - if (Multiply(static_cast(value), static_cast(candidate)) == 1) inverse = static_cast(candidate); + const auto element = static_cast(value); + std::uint8_t inverse = element; + for (int step = 0; step < 6; ++step) inverse = Multiply(Multiply(inverse, inverse), element); + inverse = Multiply(inverse, inverse); std::uint8_t result = 0x63; for (int shift = 0; shift < 5; ++shift) result ^= static_cast((inverse << shift) | (inverse >> ((8 - shift) & 7))); diff --git a/core/libs/prx/libSceLibreSsl/src/Asn1.cpp b/core/libs/prx/libSceLibreSsl/src/Asn1.cpp new file mode 100644 index 0000000000..409df8adac --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Asn1.cpp @@ -0,0 +1,128 @@ +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +static_assert(sizeof(Asn1String) == 24); +static_assert(offsetof(Asn1String, data) == 8); +static_assert(offsetof(Asn1String, flags) == 16); + +int CharacterWidth(int type) { + switch (type) { + case 12: return 0; + case 19: + case 20: + case 22: + case 23: + case 24: + case 26: return 1; + case 28: return 4; + case 30: return 2; + default: return -1; + } +} + +bool ReadCharacter(std::span& input, int width, std::uint32_t& value) { + value = 0; + if (width != 0) { + if (input.size() < static_cast(width)) return false; + for (int index = 0; index < width; ++index) value = (value << 8) | input[index]; + input = input.subspan(width); + } else { + const unsigned char lead = input.front(); + const int bytes = lead < 0x80 ? 1 : lead >= 0xc2 && lead <= 0xdf ? 2 + : lead >= 0xe0 && lead <= 0xef ? 3 : lead >= 0xf0 && lead <= 0xf4 ? 4 : 0; + if (bytes == 0 || input.size() < static_cast(bytes)) return false; + value = lead & (0x7f >> (bytes == 1 ? 0 : bytes)); + for (int index = 1; index < bytes; ++index) { + if ((input[index] & 0xc0) != 0x80) return false; + value = (value << 6) | (input[index] & 0x3f); + } + constexpr std::uint32_t minimum[] = {0, 0, 0x80, 0x800, 0x10000}; + if (value < minimum[bytes]) return false; + input = input.subspan(bytes); + } + return value <= 0x10ffff && (value < 0xd800 || value > 0xdfff); +} + +void AppendCharacter(std::vector& output, std::uint32_t value) { + const int bytes = value < 0x80 ? 1 : value < 0x800 ? 2 : value < 0x10000 ? 3 : 4; + if (output.size() >= static_cast(std::numeric_limits::max() - bytes)) + NotImplemented_nid_no_patch("ASN1_STRING_to_UTF8: output length overflow"); + if (bytes == 1) { + output.push_back(static_cast(value)); + return; + } + constexpr unsigned char prefix[] = {0, 0, 0xc0, 0xe0, 0xf0}; + output.push_back(static_cast(prefix[bytes] | (value >> (6 * (bytes - 1))))); + for (int index = bytes - 2; index >= 0; --index) + output.push_back(static_cast(0x80 | ((value >> (6 * index)) & 0x3f))); +} + +} // namespace + +extern "C" { + +std::int64_t APS5_VABI ASN1_INTEGER_get_nid_postfix(const Asn1String* value) { + if (!value) return 0; + const auto& integer = *value; + if (integer.type != 2 && integer.type != 0x102) return -1; + if (integer.length > 8) return -1; + if (integer.length <= 0 || !integer.data) return 0; + std::uint64_t magnitude = 0; + for (int index = 0; index < integer.length; ++index) magnitude = (magnitude << 8) | integer.data[index]; + const bool negative = integer.type == 0x102; + const auto limit = static_cast(std::numeric_limits::max()) + negative; + if (magnitude > limit) + NotImplemented_nid_no_patch(__func__); + if (negative && magnitude != 0) return -static_cast(magnitude - 1) - 1; + return static_cast(magnitude); +} + +unsigned char* APS5_VABI ASN1_STRING_data_nid_postfix(Asn1String* value) { + if (!value) APS5_INVALID_ARG_EX; + return value->data; +} + +int APS5_VABI ASN1_STRING_length_nid_postfix(const Asn1String* value) { + if (!value) APS5_INVALID_ARG_EX; + return value->length; +} + +int APS5_VABI ASN1_STRING_type_nid_postfix(Asn1String* value) { + if (!value) APS5_INVALID_ARG_EX; + return value->type; +} + +int APS5_VABI ASN1_STRING_to_UTF8_nid_postfix(unsigned char** output, Asn1String* value) { + if (!value) return -1; + const auto& string = *value; + const int width = CharacterWidth(string.type); + if (width < 0) return -1; + if (!output || (!string.data && string.length != 0)) NotImplemented_nid_no_patch(__func__); + std::size_t length = string.length < 0 ? std::strlen(reinterpret_cast(string.data)) + : static_cast(string.length); + if (length > static_cast(std::numeric_limits::max())) NotImplemented_nid_no_patch(__func__); + std::span input(string.data, length); + std::vector converted; + while (!input.empty()) { + std::uint32_t character; + if (!ReadCharacter(input, width, character)) NotImplemented_nid_no_patch(__func__); + AppendCharacter(converted, character); + } + auto* result = static_cast(malloc_nid_postfix(converted.size() + 1)); + if (!result) NotImplemented_nid_no_patch(__func__); + if (!converted.empty()) std::memcpy(result, converted.data(), converted.size()); + result[converted.size()] = 0; + *output = result; + return static_cast(converted.size()); +} + +} diff --git a/core/libs/prx/libSceLibreSsl/src/Cipher.cpp b/core/libs/prx/libSceLibreSsl/src/Cipher.cpp new file mode 100644 index 0000000000..bf213e3cbd --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Cipher.cpp @@ -0,0 +1,60 @@ +#include +#include +#include +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" + +namespace { + +struct Cipher { + std::int32_t valid; + const char* name; + std::uint64_t id; + std::uint64_t keyExchange; + std::uint64_t authentication; + std::uint64_t encryption; + std::uint64_t mac; + std::uint64_t protocol; + std::uint64_t strengthFlags; + std::uint64_t extraFlags; + std::int32_t strengthBits; + std::int32_t algorithmBits; +}; + +static_assert(sizeof(Cipher) == 88); +static_assert(offsetof(Cipher, name) == 8); +static_assert(offsetof(Cipher, id) == 16); +static_assert(offsetof(Cipher, strengthBits) == 80); +static_assert(offsetof(Cipher, algorithmBits) == 84); + +} // namespace + +extern "C" { + +int APS5_VABI SSL_CIPHER_get_bits_nid_postfix(const void* cipher, std::int32_t* algorithmBits) { + if (!cipher) return 0; + const auto* bytes = static_cast(cipher); + if (algorithmBits) { + std::int32_t bits; + std::memcpy(&bits, bytes + offsetof(Cipher, algorithmBits), sizeof(bits)); + *algorithmBits = bits; + } + std::int32_t strength; + std::memcpy(&strength, bytes + offsetof(Cipher, strengthBits), sizeof(strength)); + return strength; +} + +const char* APS5_VABI SSL_CIPHER_get_name_nid_postfix(const void* cipher) { + if (!cipher) return "(NONE)"; + const char* name; + std::memcpy(&name, static_cast(cipher) + offsetof(Cipher, name), sizeof(name)); + return name; +} + +char* APS5_VABI SSL_CIPHER_get_version_nid_postfix(const void* cipher) { + if (!cipher) return const_cast("(NONE)"); + std::uint64_t id; + std::memcpy(&id, static_cast(cipher) + offsetof(Cipher, id), sizeof(id)); + return const_cast((id >> 24) == 3 ? "TLSv1/SSLv3" : "unknown"); +} + +} diff --git a/core/libs/prx/libSceLibreSsl/src/Digest.cpp b/core/libs/prx/libSceLibreSsl/src/Digest.cpp index b15e61d7a0..521c9dbaa6 100644 --- a/core/libs/prx/libSceLibreSsl/src/Digest.cpp +++ b/core/libs/prx/libSceLibreSsl/src/Digest.cpp @@ -307,8 +307,6 @@ int Sha512Final(unsigned char* digest, Sha512Context& c) { StoreBig64(buffer + 120, c.Nl); Sha512Block(c, buffer); if (!digest) return 0; - if (c.mdLength == Sha224Length || c.mdLength == Sha256Length) - NotImplemented_nid_no_patch("SHA512_Final: SHA-512/224 and SHA-512/256 digest lengths"); if (c.mdLength != Sha384Length && c.mdLength != Sha512Length) return 0; for (std::size_t i = 0; i < c.mdLength / 8; ++i) StoreBig64(digest + 8 * i, c.h[i]); return 1; @@ -457,8 +455,9 @@ int APS5_VABI SHA224_Final_nid_postfix(unsigned char* digest, Sha256Context* con } unsigned char* APS5_VABI SHA256_nid_postfix(const unsigned char* data, std::size_t length, unsigned char* digest) { + static unsigned char buffer[Sha256Length]; RequireInput(data, length, __func__); - if (!digest) NotImplemented_nid_no_patch("SHA256: null digest buffer"); + if (!digest) digest = buffer; Sha256Context c; SHA256_Init_nid_postfix(&c); diff --git a/core/libs/prx/libSceLibreSsl/src/Error.cpp b/core/libs/prx/libSceLibreSsl/src/Error.cpp new file mode 100644 index 0000000000..bad0374dbb --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Error.cpp @@ -0,0 +1,64 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" +#include +#include +#include +#include "prx/libSceLibreSsl/include/Error.hpp" +#include "prx/libc/include/General.hpp" + +namespace { +constexpr int QueueSize = 16; + +struct ErrorQueue { + std::array codes; + int top; + int bottom; +}; + +static_assert(std::is_trivially_destructible_v); + +thread_local ErrorQueue queue{}; + +std::uint64_t Oldest(bool remove) { + if (queue.bottom == queue.top) return 0; + const int index = (queue.bottom + 1) % QueueSize; + const std::uint64_t code = queue.codes[index]; + if (remove) { + queue.bottom = index; + queue.codes[index] = 0; + } + return code; +} + +void Clear() { + queue = {}; +} +} + +extern "C" { + +void LibreSslPutError_nid_no_patch(int library, int function, int reason) { + queue.top = (queue.top + 1) % QueueSize; + if (queue.top == queue.bottom) queue.bottom = (queue.bottom + 1) % QueueSize; + queue.codes[queue.top] = (static_cast(library) & 0xff) << 24 | + (static_cast(function) & 0xfff) << 12 | + (static_cast(reason) & 0xfff); +} + +std::uint64_t APS5_VABI ERR_get_error_nid_postfix() { + return Oldest(true); +} + +std::uint64_t APS5_VABI ERR_peek_error_nid_postfix() { + return Oldest(false); +} + +void APS5_VABI ERR_clear_error_nid_postfix() { + Clear(); +} + +void APS5_VABI ERR_remove_thread_state_nid_postfix(const void* threadId) { + if (threadId != nullptr) NotImplemented_nid_no_patch("ERR_remove_thread_state of a CRYPTO_THREADID"); + Clear(); +} + +} diff --git a/core/libs/prx/libSceLibreSsl/src/Ocsp.cpp b/core/libs/prx/libSceLibreSsl/src/Ocsp.cpp new file mode 100644 index 0000000000..e64c504978 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Ocsp.cpp @@ -0,0 +1,57 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" +#include +#include + +namespace { +struct OcspString { + std::int64_t code; + const char* text; +}; + +template +const char* TableToString(std::int64_t code, const OcspString (&table)[Count]) { + for (const auto& entry : table) { + if (entry.code == code) return entry.text; + } + return "(UNKNOWN)"; +} +} + +extern "C" { + +const char* APS5_VABI OCSP_response_status_str_nid_postfix(std::int64_t status) { + static constexpr OcspString table[] { + {0, "successful"}, + {1, "malformedrequest"}, + {2, "internalerror"}, + {3, "trylater"}, + {5, "sigrequired"}, + {6, "unauthorized"}, + }; + return TableToString(status, table); +} + +const char* APS5_VABI OCSP_cert_status_str_nid_postfix(std::int64_t status) { + static constexpr OcspString table[] { + {0, "good"}, + {1, "revoked"}, + {2, "unknown"}, + }; + return TableToString(status, table); +} + +const char* APS5_VABI OCSP_crl_reason_str_nid_postfix(std::int64_t reason) { + static constexpr OcspString table[] { + {0, "unspecified"}, + {1, "keyCompromise"}, + {2, "cACompromise"}, + {3, "affiliationChanged"}, + {4, "superseded"}, + {5, "cessationOfOperation"}, + {6, "certificateHold"}, + {8, "removeFromCRL"}, + }; + return TableToString(reason, table); +} + +} diff --git a/core/libs/prx/libSceLibreSsl/src/Stack.cpp b/core/libs/prx/libSceLibreSsl/src/Stack.cpp new file mode 100644 index 0000000000..f25595eec3 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Stack.cpp @@ -0,0 +1,56 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include "prx/libc/include/General.hpp" + +static_assert(sizeof(LibreSslStack) == 32); +static_assert(offsetof(LibreSslStack, num) == 0); +static_assert(offsetof(LibreSslStack, data) == 8); +static_assert(offsetof(LibreSslStack, sorted) == 16); +static_assert(offsetof(LibreSslStack, numAlloc) == 20); +static_assert(offsetof(LibreSslStack, comp) == 24); + +namespace { + +char** Elements(const LibreSslStack* stack, const char* function) { + if (!stack->data) throw std::invalid_argument(std::string(function) + ": null element array"); + return stack->data; +} + +} + +extern "C" { +int APS5_VABI sk_num_nid_postfix(const LibreSslStack* stack) { + if (!stack) return -1; + return stack->num; +} + +void* APS5_VABI sk_value_nid_postfix(const LibreSslStack* stack, int index) { + if (!stack || index < 0 || index >= stack->num) return nullptr; + return Elements(stack, __func__)[index]; +} + +void* APS5_VABI sk_pop_nid_postfix(LibreSslStack* stack) { + if (!stack || stack->num <= 0) return nullptr; + char* const element = Elements(stack, __func__)[stack->num - 1]; + --stack->num; + return element; +} + +void APS5_VABI sk_pop_free_nid_postfix(LibreSslStack* stack, StackElementFree release) { + if (!stack) return; + if (stack->num > 0) { + char** const elements = Elements(stack, __func__); + for (int index = 0; index < stack->num; ++index) { + if (!elements[index]) continue; + if (!release) throw std::invalid_argument(std::string(__func__) + ": null free function"); + release(elements[index]); + } + } + free_nid_postfix(stack->data); + free_nid_postfix(stack); +} + +} diff --git a/core/libs/prx/libSceLibreSsl/src/Ui.cpp b/core/libs/prx/libSceLibreSsl/src/Ui.cpp new file mode 100644 index 0000000000..1cbbcdc084 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Ui.cpp @@ -0,0 +1,71 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include "prx/libc/include/General.hpp" + +static_assert(sizeof(LibreSslUiMethod) == 56); + +extern "C" { +LibreSslUiMethod* APS5_VABI UI_create_method_nid_postfix(char* name) { + auto* method = static_cast(malloc_nid_postfix(sizeof(LibreSslUiMethod))); + if (!method) return nullptr; + std::memset(method, 0, sizeof(LibreSslUiMethod)); + if (name) { + const std::size_t size = std::strlen(name) + 1; + method->name = static_cast(malloc_nid_postfix(size)); + if (method->name) std::memcpy(method->name, name, size); + } + return method; +} + +void APS5_VABI UI_destroy_method_nid_postfix(LibreSslUiMethod* method) { + if (!method) throw std::invalid_argument(std::string(__func__) + ": null method"); + free_nid_postfix(method->name); + method->name = nullptr; + free_nid_postfix(method); +} + +int APS5_VABI UI_method_set_opener_nid_postfix(LibreSslUiMethod* method, UiSessionFunction opener) { + if (!method) return -1; + method->openSession = opener; + return 0; +} + +UiSessionFunction APS5_VABI UI_method_get_opener_nid_postfix(const LibreSslUiMethod* method) { + return method ? method->openSession : nullptr; +} + +int APS5_VABI UI_method_set_writer_nid_postfix(LibreSslUiMethod* method, UiStringFunction writer) { + if (!method) return -1; + method->writeString = writer; + return 0; +} + +UiStringFunction APS5_VABI UI_method_get_writer_nid_postfix(const LibreSslUiMethod* method) { + return method ? method->writeString : nullptr; +} + +int APS5_VABI UI_method_set_reader_nid_postfix(LibreSslUiMethod* method, UiStringFunction reader) { + if (!method) return -1; + method->readString = reader; + return 0; +} + +UiStringFunction APS5_VABI UI_method_get_reader_nid_postfix(const LibreSslUiMethod* method) { + return method ? method->readString : nullptr; +} + +int APS5_VABI UI_method_set_closer_nid_postfix(LibreSslUiMethod* method, UiSessionFunction closer) { + if (!method) return -1; + method->closeSession = closer; + return 0; +} + +UiSessionFunction APS5_VABI UI_method_get_closer_nid_postfix(const LibreSslUiMethod* method) { + return method ? method->closeSession : nullptr; +} + +} diff --git a/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp b/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp new file mode 100644 index 0000000000..e99142e9ad --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp @@ -0,0 +1,231 @@ +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +static_assert(sizeof(Asn1String) == 24); +static_assert(offsetof(Asn1String, data) == 8); +static_assert(offsetof(Asn1String, flags) == 16); + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "failed: %s\n", message); + std::abort(); +} + +template +void RequireThrows(TAction action, const char* message) { + try { + action(); + } catch (const std::exception&) { + return; + } + Require(false, message); +} + +template +void RequireInvalidArgument(TAction action, const char* message) { + try { + action(); + } catch (const std::invalid_argument&) { + return; + } + Require(false, message); +} + +void CheckConversion(int type, std::span input, std::span expected, int length = -2) { + std::vector storage(input.begin(), input.end()); + storage.push_back(0); + Asn1String value{length == -2 ? static_cast(input.size()) : length, type, storage.data(), 0x123456789abcdef}; + const Asn1String original = value; + unsigned char sentinel = 0xab; + unsigned char* result = &sentinel; + Require(ASN1_STRING_to_UTF8_nid_postfix(&result, &value) == static_cast(expected.size()), "UTF-8 byte length"); + Require(result && result != &sentinel && result != storage.data(), "conversion owns a new buffer"); + Require(expected.empty() || std::memcmp(result, expected.data(), expected.size()) == 0, "converted UTF-8 bytes"); + Require(result[expected.size()] == 0, "conversion terminator"); + Require(value.length == original.length && value.type == original.type && value.data == original.data + && value.flags == original.flags, "conversion preserves the input object"); + Require(std::equal(input.begin(), input.end(), storage.begin()), "conversion preserves the input bytes"); + free_nid_postfix(result); +} + +void Accessors() { + unsigned char data[] = {'a', 0, 'b'}; + Asn1String value{3, 12, data, std::numeric_limits::max()}; + Require(ASN1_STRING_data_nid_postfix(&value) == data, "data accessor preserves pointer identity"); + ASN1_STRING_data_nid_postfix(&value)[2] = 'c'; + Require(data[2] == 'c', "data accessor permits writes"); + Require(ASN1_STRING_length_nid_postfix(&value) == 3, "length includes embedded NUL"); + Require(ASN1_STRING_type_nid_postfix(&value) == 12, "type accessor"); + value = {-9, 0x7fffffff, nullptr, -1}; + Require(ASN1_STRING_data_nid_postfix(&value) == nullptr, "null data is preserved"); + Require(ASN1_STRING_length_nid_postfix(&value) == -9, "length is returned without normalization"); + Require(ASN1_STRING_type_nid_postfix(&value) == 0x7fffffff, "type is returned without normalization"); + RequireInvalidArgument([] { ASN1_STRING_data_nid_postfix(nullptr); }, "null data accessor input"); + RequireInvalidArgument([] { ASN1_STRING_length_nid_postfix(nullptr); }, "null length accessor input"); + RequireInvalidArgument([] { ASN1_STRING_type_nid_postfix(nullptr); }, "null type accessor input"); +} + +void Integers() { + Require(ASN1_INTEGER_get_nid_postfix(nullptr) == 0, "null integer"); + unsigned char data[] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0}; + constexpr std::array expected{ + 0, 0x01, 0x0123, 0x012345, 0x01234567, 0x0123456789, 0x0123456789ab, + 0x0123456789abcd, 0x0123456789abcdef}; + for (int length = 0; length <= 8; ++length) { + Asn1String value{length, 2, data, -1}; + Require(ASN1_INTEGER_get_nid_postfix(&value) == expected[length], "positive integer and 64-bit guest long"); + value.type = 0x102; + Require(ASN1_INTEGER_get_nid_postfix(&value) == -expected[length], "negative integer magnitude"); + } + Asn1String value{9, 2, nullptr, 0}; + Require(ASN1_INTEGER_get_nid_postfix(&value) == -1, "overlength is rejected before null data"); + value.length = 8; + Require(ASN1_INTEGER_get_nid_postfix(&value) == 0, "integer with null data"); + for (int length : {std::numeric_limits::min(), -8, -1, 0}) { + for (int type : {2, 0x102}) { + for (auto* bytes : {data, static_cast(nullptr), reinterpret_cast(1)}) { + value = {length, type, bytes, -1}; + const Asn1String original = value; + Require(ASN1_INTEGER_get_nid_postfix(&value) == 0, "LibreSSL 2.5.4 nonpositive integer length"); + Require(value.length == original.length && value.type == original.type && value.data == original.data + && value.flags == original.flags, "nonpositive integer length preserves input"); + } + } + } + value = {-1, 2, data, 0}; + for (int type : {-1, 0, 1, 3, 12, 0x100, 0x202, 0x302}) { + value.type = type; + Require(ASN1_INTEGER_get_nid_postfix(&value) == -1, "only integer types are accepted"); + } + std::array maximum{0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; + value = {8, 2, maximum.data(), 0}; + Require(ASN1_INTEGER_get_nid_postfix(&value) == std::numeric_limits::max(), "maximum signed integer"); + value.type = 0x102; + Require(ASN1_INTEGER_get_nid_postfix(&value) == -std::numeric_limits::max(), "negative maximum magnitude"); + std::array minimum{0x80, 0, 0, 0, 0, 0, 0, 0}; + value = {8, 0x102, minimum.data(), -1}; + const Asn1String original = value; + Require(ASN1_INTEGER_get_nid_postfix(&value) == std::numeric_limits::min(), "minimum signed integer"); + Require(value.length == original.length && value.type == original.type && value.data == original.data + && value.flags == original.flags && minimum == std::array{0x80, 0, 0, 0, 0, 0, 0, 0}, "minimum integer preserves input"); + value.type = 2; + RequireThrows([&] { ASN1_INTEGER_get_nid_postfix(&value); }, "positive magnitude above signed maximum"); + value.type = 0x102; + minimum.back() = 1; + RequireThrows([&] { ASN1_INTEGER_get_nid_postfix(&value); }, "negative magnitude below signed minimum"); + value.data = maximum.data(); + maximum[0] = 0x80; + for (int type : {2, 0x102}) { + value.type = type; + RequireThrows([&] { ASN1_INTEGER_get_nid_postfix(&value); }, "source signed-overflow path throws"); + } + std::array zero{}; + value = {9, 2, zero.data(), 0}; + Require(ASN1_INTEGER_get_nid_postfix(&value) == -1, "overlength leading zeros are not trimmed"); +} + +void Strings() { + constexpr unsigned char latin[] = {0, 0x41, 0x7f, 0x80, 0xa3, 0xff}; + constexpr unsigned char latinUtf8[] = {0, 0x41, 0x7f, 0xc2, 0x80, 0xc2, 0xa3, 0xc3, 0xbf}; + constexpr unsigned char bmp[] = {0, 0, 0, 0x7f, 0, 0x80, 7, 0xff, 8, 0, 0x20, 0xac, 0xd7, 0xff, 0xe0, 0, 0xff, 0xff}; + constexpr unsigned char bmpUtf8[] = {0, 0x7f, 0xc2, 0x80, 0xdf, 0xbf, 0xe0, 0xa0, 0x80, 0xe2, 0x82, 0xac, + 0xed, 0x9f, 0xbf, 0xee, 0x80, 0x80, 0xef, 0xbf, 0xbf}; + constexpr unsigned char universal[] = {0, 0, 0, 0x41, 0, 1, 0, 0, 0, 1, 0xf6, 0, 0, 0x10, 0xff, 0xff}; + constexpr unsigned char universalUtf8[] = {0x41, 0xf0, 0x90, 0x80, 0x80, 0xf0, 0x9f, 0x98, 0x80, 0xf4, 0x8f, 0xbf, 0xbf}; + for (int type : {19, 20, 22, 23, 24, 26}) CheckConversion(type, latin, latinUtf8); + CheckConversion(30, bmp, bmpUtf8); + CheckConversion(28, universal, universalUtf8); + CheckConversion(12, bmpUtf8, bmpUtf8); + CheckConversion(12, universalUtf8, universalUtf8); + for (int type : {12, 19, 20, 22, 23, 24, 26, 28, 30}) { + CheckConversion(type, {}, {}); + Asn1String empty{0, type, nullptr, 0}; + unsigned char* result = nullptr; + Require(ASN1_STRING_to_UTF8_nid_postfix(&result, &empty) == 0 && result && *result == 0, "empty null-data string"); + free_nid_postfix(result); + } + constexpr unsigned char terminated[] = {'A', 0xc2, 0xa3, 0, 'X'}; + CheckConversion(12, terminated, std::span(terminated).first(3), -1); + CheckConversion(12, terminated, std::span(terminated).first(3), -7); + constexpr unsigned char ascii[] = {'A', 'B', 'C'}; + CheckConversion(19, ascii, ascii, -1); + + unsigned char sentinel = 0xab; + unsigned char* result = &sentinel; + Require(ASN1_STRING_to_UTF8_nid_postfix(&result, nullptr) == -1 && result == &sentinel, "null conversion input"); + Require(ASN1_STRING_to_UTF8_nid_postfix(nullptr, nullptr) == -1, "null input checked before output"); + for (int type = -1; type <= 32; ++type) { + if (type == 12 || type == 19 || type == 20 || type == 22 || type == 23 || type == 24 || type == 26 || type == 28 || type == 30) continue; + Asn1String value{0, type, nullptr, 0}; + Require(ASN1_STRING_to_UTF8_nid_postfix(&result, &value) == -1 && result == &sentinel, "unsupported string tag"); + Require(ASN1_STRING_to_UTF8_nid_postfix(nullptr, &value) == -1, "tag checked before output"); + } + Asn1String value{0, 12, nullptr, 0}; + RequireThrows([&] { ASN1_STRING_to_UTF8_nid_postfix(nullptr, &value); }, "null conversion output"); + value.length = 1; + RequireThrows([&] { ASN1_STRING_to_UTF8_nid_postfix(&result, &value); }, "null nonempty string data"); + value.length = -1; + RequireThrows([&] { ASN1_STRING_to_UTF8_nid_postfix(&result, &value); }, "null terminated-string data"); +} + +std::size_t lastAllocationSize = 0; +int allocationCount = 0; +int freeCount = 0; + +void* APS5_VABI Allocate_nid_no_patch(std::size_t size) { + lastAllocationSize = size; + ++allocationCount; + return std::malloc(size); +} + +void APS5_VABI Free_nid_no_patch(void* pointer) { + ++freeCount; + std::free(pointer); +} + +void* APS5_VABI Calloc_nid_no_patch(std::size_t, std::size_t) { throw std::runtime_error("unexpected calloc"); } +void* APS5_VABI Reallocate_nid_no_patch(void*, std::size_t) { throw std::runtime_error("unexpected realloc"); } +void* APS5_VABI Align_nid_no_patch(std::size_t, std::size_t) { throw std::runtime_error("unexpected aligned allocation"); } +void* APS5_VABI Realign_nid_no_patch(void*, std::size_t, std::size_t) { throw std::runtime_error("unexpected aligned reallocation"); } +int APS5_VABI PosixAlign_nid_no_patch(void**, std::size_t, std::size_t) { throw std::runtime_error("unexpected posix_memalign"); } + +void Allocator() { + std::array api{ + reinterpret_cast(Allocate_nid_no_patch), reinterpret_cast(Free_nid_no_patch), reinterpret_cast(Calloc_nid_no_patch), + reinterpret_cast(Reallocate_nid_no_patch), reinterpret_cast(Align_nid_no_patch), reinterpret_cast(Realign_nid_no_patch), + reinterpret_cast(PosixAlign_nid_no_patch)}; + ApplicationHeapRegister_nid_no_patch(api.data()); + constexpr unsigned char input[] = {0xff}; + constexpr unsigned char expected[] = {0xc3, 0xbf}; + CheckConversion(20, input, expected); + Require(allocationCount == 1 && lastAllocationSize == 3 && freeCount == 1, "conversion uses the application allocator"); +} + +} // namespace + +int main(int argc, char**) { + if (argc > 1) { + Allocator(); + return 0; + } + std::array api{}; + ApplicationHeapRegister_nid_no_patch(api.data()); + Accessors(); + Integers(); + Strings(); +} diff --git a/core/libs/prx/libSceLibreSsl3/src/Digest.cpp b/core/libs/prx/libSceLibreSsl3/src/Digest.cpp index ae6a5b5e19..0df5d8b0b4 100644 --- a/core/libs/prx/libSceLibreSsl3/src/Digest.cpp +++ b/core/libs/prx/libSceLibreSsl3/src/Digest.cpp @@ -1,12 +1,13 @@ #include "prx/libSceLibreSsl3/include/Api.hpp" #include "prx/libSceLibreSsl/include/Api.hpp" #include +#include #include "prx/libc/include/General.hpp" extern "C" { unsigned char* APS5_VABI MD5_nid_postfix(const unsigned char* data, std::size_t length, unsigned char* digest) { - if (!digest) NotImplemented_nid_no_patch("MD5: null digest buffer"); + if (!digest) throw std::invalid_argument("MD5: null digest buffer"); Md5Context context; MD5_Init_nid_postfix(&context); MD5_Update_nid_postfix(&context, data, length); @@ -16,7 +17,7 @@ unsigned char* APS5_VABI MD5_nid_postfix(const unsigned char* data, std::size_t } unsigned char* APS5_VABI SHA1_nid_postfix(const unsigned char* data, std::size_t length, unsigned char* digest) { - if (!digest) NotImplemented_nid_no_patch("SHA1: null digest buffer"); + if (!digest) throw std::invalid_argument("SHA1: null digest buffer"); Sha1Context context; SHA1_Init_nid_postfix(&context); SHA1_Update_nid_postfix(&context, data, length); diff --git a/core/libs/prx/libSceMove/Export.cpp b/core/libs/prx/libSceMove/Export.cpp index b69438284d..e55eded120 100644 --- a/core/libs/prx/libSceMove/Export.cpp +++ b/core/libs/prx/libSceMove/Export.cpp @@ -1,9 +1,11 @@ #include "prx/libSceMove/include/Api.hpp" #include #include +#include #include #include #include "SceTypes.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceUserService/UserService.hpp" @@ -18,6 +20,7 @@ constexpr int MOVE_ERROR_INVALID_HANDLE = static_cast(0x80EE0004); constexpr int MOVE_ERROR_ALREADY_OPENED = static_cast(0x80EE0007); constexpr int32_t MOVE_FIRST_HANDLE = 0x30B0100; constexpr int32_t MOVE_HANDLE_STEP = 0x100; +constexpr uint32_t MOVE_LAST_SDK_ALLOWING_NULL_DATA = 0x03ffffff; std::mutex stateMutex; bool initialized = false; @@ -37,6 +40,13 @@ int checkHandle(int32_t handle, bool validArguments = true) { return MOVE_OK; } +uint32_t compiledSdkVersion() { + static const auto* processParameters = static_cast(ApplicationProcessParameters_nid_no_patch()); + uint32_t version; + std::memcpy(&version, processParameters + 0x10, sizeof(version)); + return version; +} + } extern "C" { @@ -91,7 +101,8 @@ int APS5_VABI sceMoveReadStateLatest(int32_t handle, void* data) { } int APS5_VABI sceMoveReadStateRecent(int32_t handle, int64_t timestamp, void* data, int32_t* outCount) { - if (const int result = checkHandle(handle, timestamp >= 0 && data != nullptr && outCount != nullptr); result != MOVE_OK) return result; + const bool dataAccepted = data != nullptr || compiledSdkVersion() <= MOVE_LAST_SDK_ALLOWING_NULL_DATA; + if (const int result = checkHandle(handle, timestamp >= 0 && dataAccepted && outCount != nullptr); result != MOVE_OK) return result; return MOVE_NO_CONTROLLER_CONNECTED; } diff --git a/core/libs/prx/libSceMsgDialog/Export.cpp b/core/libs/prx/libSceMsgDialog/Export.cpp index a94de137f5..c9462f47b7 100644 --- a/core/libs/prx/libSceMsgDialog/Export.cpp +++ b/core/libs/prx/libSceMsgDialog/Export.cpp @@ -1,7 +1,4 @@ #include "prx/libSceMsgDialog/include/Api.hpp" -// libSceMsgDialog: message dialog state machine (NONE -> INITIALIZED -> RUNNING -> FINISHED) without a visible dialog. -// The dialog "closes" with the affirmative button after a couple of status polls, so a title waiting for the user -// never blocks. The message text is logged for the first calls. #include #include #include @@ -40,8 +37,8 @@ constexpr int kButtonTypeNone = 2; constexpr int kButtonTypeOkCancel = 3; constexpr int kButtonTypeYesNoFocusNo = 4; -constexpr int kButtonIdOk = 1; // OK / YES / BUTTON1 -constexpr int kButtonIdNo = 2; // NO / BUTTON2 +constexpr int kButtonIdOk = 1; +constexpr int kButtonIdNo = 2; std::mutex g_lock; int g_status = kStatusNone; @@ -63,8 +60,6 @@ bool Readable(const void* p, std::size_t n) { #endif } -// Best-effort inspection of SceMsgDialogParam: { SceCommonDialogBaseParam base (0x30); size_t size; int mode (0x38); -// UserMessageParam* userMsg (0x40); ProgressBarParam* progress (0x48); SystemMessageParam* system (0x50); ... }. void Inspect(const void* param) { const std::uint8_t* bytes = static_cast(param); g_mode = kModeUserMsg; @@ -94,7 +89,6 @@ int Poll() { if (g_status == kStatusRunning) { bool done = false; if (g_mode == kModeProgressBar) { - // Progress dialogs are ended by the title (sceMsgDialogClose); the cap only prevents a wait without end. done = std::chrono::steady_clock::now() - g_openedAt > std::chrono::seconds(30); } else { done = --g_pollsLeft <= 0; @@ -103,7 +97,7 @@ int Poll() { } return g_status; } -} // namespace +} extern "C" { @@ -123,7 +117,6 @@ int APS5_VABI sceMsgDialogOpen(const void* param) { if (param == nullptr) return kErrArgNull; std::lock_guard g(g_lock); if (g_status == kStatusRunning) return kErrBusy; - // A title that skipped sceMsgDialogInitialize still gets its dialog (and its FINISHED status). Inspect(param); g_pollsLeft = 2; g_openedAt = std::chrono::steady_clock::now(); diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index dbf0992fae..90129eac83 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -4,6 +4,26 @@ target_link_libraries(libSceNet PRIVATE libSceNetCtl) configure_windows_unwind(libSceNet) if(BUILD_TESTING) + add_test_executable(guest_net_socket_state_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetSocketState.cpp) + target_include_directories(guest_net_socket_state_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_socket_state_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_net_socket_state_tests) + add_test(NAME guest_net_socket_state COMMAND guest_net_socket_state_tests) + set_tests_properties(guest_net_socket_state PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_net_socket_state PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_net_protocol_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetProtocol.cpp) + target_include_directories(guest_net_protocol_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_protocol_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_net_protocol_tests) + add_test(NAME guest_net_protocol COMMAND guest_net_protocol_tests) + set_tests_properties(guest_net_protocol PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_net_protocol PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(net_dontwait_tests ${LIBS_INCLUDE_DIR}/tests/NetDontWait.cpp) target_include_directories(net_dontwait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(net_dontwait_tests PRIVATE libSceNet libkernel libc) @@ -27,6 +47,17 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test_executable(guest_net_boolean_readback_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetBooleanReadback.cpp) + target_include_directories(guest_net_boolean_readback_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_boolean_readback_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_net_boolean_readback_tests) + add_test(NAME guest_net_boolean_readback COMMAND guest_net_boolean_readback_tests) + set_tests_properties(guest_net_boolean_readback PROPERTIES TIMEOUT 20) + if(WIN32) + target_link_libraries(guest_net_boolean_readback_tests PRIVATE ws2_32) + set_tests_properties(guest_net_boolean_readback PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(net_blocking_send_tests ${LIBS_INCLUDE_DIR}/tests/NetBlockingSend.cpp) target_include_directories(net_blocking_send_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(net_blocking_send_tests PRIVATE libkernel libc libSceNetCtl) @@ -42,6 +73,17 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test_executable(guest_net_keepalive_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetKeepalive.cpp) + target_include_directories(guest_net_keepalive_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_keepalive_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_net_keepalive_tests) + add_test(NAME guest_sce_net_keepalive COMMAND guest_net_keepalive_tests) + set_tests_properties(guest_sce_net_keepalive PROPERTIES TIMEOUT 20) + if(WIN32) + target_link_libraries(guest_net_keepalive_tests PRIVATE ws2_32) + set_tests_properties(guest_sce_net_keepalive 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) @@ -54,6 +96,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test_executable(guest_net_dns_info_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetDnsInfo.cpp) + target_include_directories(guest_net_dns_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_dns_info_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_net_dns_info_tests) + add_test(NAME guest_net_dns_info COMMAND guest_net_dns_info_tests) + set_tests_properties(guest_net_dns_info PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_net_dns_info PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_refused_connect_tests ${LIBS_INCLUDE_DIR}/tests/GuestRefusedConnect.cpp) target_include_directories(guest_refused_connect_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_refused_connect_tests PRIVATE libSceNet libkernel libc) @@ -64,6 +116,17 @@ if(BUILD_TESTING) set_tests_properties(guest_refused_connect PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_net_oob_inline_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetOobInline.cpp) + target_include_directories(guest_net_oob_inline_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_oob_inline_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_net_oob_inline_tests) + add_test(NAME guest_net_oob_inline COMMAND guest_net_oob_inline_tests) + set_tests_properties(guest_net_oob_inline PROPERTIES TIMEOUT 20) + if(WIN32) + target_link_libraries(guest_net_oob_inline_tests PRIVATE ws2_32) + set_tests_properties(guest_net_oob_inline PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() if(WIN32) add_test_executable(guest_socket_host_write_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketHostWrite.cpp) target_include_directories(guest_socket_host_write_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -107,6 +170,17 @@ if(BUILD_TESTING) set_tests_properties(guest_sce_net guest_net_thread PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_net_nodelay_readback_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetNodelayReadback.cpp) + target_include_directories(guest_net_nodelay_readback_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_nodelay_readback_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_net_nodelay_readback_tests) + add_test(NAME guest_net_nodelay_readback COMMAND guest_net_nodelay_readback_tests) + set_tests_properties(guest_net_nodelay_readback PROPERTIES TIMEOUT 20) + if(WIN32) + target_link_libraries(guest_net_nodelay_readback_tests PRIVATE ws2_32) + set_tests_properties(guest_net_nodelay_readback PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(net_epoll_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/NetEpollLifetime.cpp) target_include_directories(net_epoll_lifetime_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(net_epoll_lifetime_tests PRIVATE libkernel libc libSceNetCtl) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index f487e1044d..ad5ab932d3 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -143,14 +143,19 @@ constexpr int NET_TF_NODELAY = 4; constexpr int NET_SO_SNDTIMEO = 0x1005; constexpr int NET_SO_RCVTIMEO = 0x1006; constexpr int NET_SO_ERROR = 0x1007; +constexpr int NET_SO_PROTOCOL = 0x1016; constexpr int NET_SO_NBIO = 0x1200; constexpr int NET_SO_REUSEADDR = 0x4; +constexpr int NET_SO_KEEPALIVE = 0x8; constexpr int NET_SO_BROADCAST = 0x20; constexpr int NET_SO_ONESBCAST = 0x10000; +constexpr int NET_SO_OOBINLINE = 0x100; constexpr int NET_SO_SNDBUF = 0x1001; constexpr int NET_SO_RCVBUF = 0x1002; constexpr int NET_SO_SCE_SNDTIMEO = 0x1105; constexpr int NET_SO_SCE_RCVTIMEO = 0x1106; +constexpr int NET_SO_TYPE = 0x1008; +constexpr int NET_SO_ACCEPTCONN = 0x2; constexpr int NET_MSG_PEEK = 0x2; constexpr int NET_MSG_TRUNC = 0x10; constexpr int NET_MSG_DONTWAIT = 0x80; @@ -260,16 +265,20 @@ bool native_to_guest_address(const sockaddr_storage& native, void* address, std: struct Sock { int family = 0; int type = 0; + int protocol = 0; std::shared_ptr native; bool bound = false; bool listening = false; bool connectAttempted = false; bool aborted = false; + bool sending_shut = false; bool read_shut = false; + bool oobInline = false; int reported_error = 0; int snd_buffer = 0; int rcv_buffer = 0; int onesbcast = 0; + int keepalive = 0; }; #ifdef _WIN32 @@ -353,6 +362,15 @@ int* errno_slot() { extern "C" std::size_t NetCtlHostDnsServers_nid_no_patch(std::uint32_t* servers, std::size_t capacity); +std::mutex g_dnsLock; +bool g_dnsOverridden = false; +NetDnsInfo g_dnsOverride{}; +NetDns6Info g_dns6Override{}; + +bool IsZeroAddress(const NetIpv6Address& address) { + return std::all_of(std::begin(address.bytes), std::end(address.bytes), [](std::uint8_t byte) { return byte == 0; }); +} + void set_errno(int err) { *errno_slot() = err; *__error_nid_postfix() = err; @@ -461,6 +479,14 @@ int timeout_option(int optname) { return optname; } +#ifdef _WIN32 +short closed_stream_events(const Sock& socket, short native, short requested) { + if (socket.type != NET_SOCK_STREAM || (native & (POLLHUP | POLLERR)) != POLLHUP) return native; + native = static_cast(native | (requested & POLLRDNORM)); + return socket.sending_shut ? native : static_cast(native & ~POLLHUP); +} +#endif + std::int64_t message_length(const NetMsghdr* message) { if (!message) return fail(NET_EFAULT); if (message->iov_length < 0 || message->iov_length > NET_UIO_MAXIOV) return fail(NET_EMSGSIZE); @@ -523,6 +549,24 @@ auto native_transfer(const NativeSocketHandle& socket, int flags, bool send, TOp #endif } +#ifdef __APPLE__ +template +std::int64_t send_stream_in_chunks(const NativeSocketHandle& socket, int flags, const char* data, std::size_t length, TSend sendChunk) { + if (!socket.stream || (flags & NET_MSG_DONTWAIT) != 0 || socket.nonblock.load()) return sendChunk(data, length); + if (length == 0) return sendChunk(data, 0); + constexpr std::size_t Chunk = 65536; + std::size_t transferred = 0; + do { + const auto request = std::min(Chunk, length - transferred); + const auto result = sendChunk(data + transferred, request); + if (result < 0) return transferred > 0 ? static_cast(transferred) : -1; + transferred += static_cast(result); + if (static_cast(result) < request) break; + } while (transferred < length); + return static_cast(transferred); +} +#endif + #ifdef _WIN32 int TransferSocketForKernel(KernelSocketPoll::NativeOwner* owner, bool send, void* buffer, int length) { const auto* socket = static_cast(owner); @@ -537,6 +581,10 @@ int native_message_flags(int flags) { int native = (flags & NET_MSG_PEEK) ? MSG_PEEK : 0; #ifndef _WIN32 if (flags & NET_MSG_DONTWAIT) native |= MSG_DONTWAIT; +#endif +#ifdef __APPLE__ + constexpr int XnuMessageNonblocking = 0x20000; + if (flags & NET_MSG_DONTWAIT) native |= XnuMessageNonblocking; #endif return native; } @@ -891,6 +939,9 @@ int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol) Sock s; s.family = family; s.type = type; + s.protocol = protocol; + if (protocol == 0 && type == NET_SOCK_STREAM) s.protocol = IPPROTO_TCP; + if (protocol == 0 && type == NET_SOCK_DGRAM) s.protocol = IPPROTO_UDP; s.native = std::move(handle); g_socks[fd] = s; return fd; @@ -1013,6 +1064,7 @@ int APS5_VABI sceNetAccept(int s, void* addr, uint32_t* addrlen) { Sock connection; connection.family = listener.family; connection.type = NET_SOCK_STREAM; + connection.protocol = listener.protocol; connection.connectAttempted = true; connection.native = std::move(handle); #ifdef _WIN32 @@ -1153,6 +1205,10 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { return result >= 0 ? result : fail(native_error()); } +int64_t APS5_VABI GuestNetRecv_nid_no_patch(int s, void* buf, size_t len, int flags) { + return sceNetRecv(s, buf, len, flags); +} + int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* from, uint32_t* fromlen) { if (!message_flags_supported(flags, NET_MSG_PEEK | NET_MSG_DONTWAIT)) return fail(NET_EOPNOTSUPP); if (!buf && len) return fail(NET_EINVAL); @@ -1207,12 +1263,22 @@ int64_t APS5_VABI sceNetSend(int s, const void* buf, size_t len, int flags) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } +#ifdef __APPLE__ + const auto result = send_stream_in_chunks(*socket.native, flags, static_cast(buf), len, [&](const char* data, std::size_t size) { + return ::send(socket.native->value, data, size, NATIVE_SEND_FLAGS | native_message_flags(flags)); + }); +#else const int result = native_transfer(*socket.native, flags, true, [&](int offset) { return ::send(socket.native->value, offset ? static_cast(buf) + offset : static_cast(buf), static_cast(len) - offset, NATIVE_SEND_FLAGS | native_message_flags(flags)); }, true, static_cast(len)); +#endif return result >= 0 ? result : fail(native_error()); } +int64_t APS5_VABI GuestNetSend_nid_no_patch(int s, const void* buf, size_t len, int flags) { + return sceNetSend(s, buf, len, flags); +} + int64_t APS5_VABI sceNetSendto(int s, const void* buf, size_t len, int flags, const void* to, uint32_t tolen) { if (!message_flags_supported(flags, NET_MSG_DONTWAIT)) return fail(NET_EOPNOTSUPP); if (!buf && len) return fail(NET_EINVAL); @@ -1224,21 +1290,38 @@ int64_t APS5_VABI sceNetSendto(int s, const void* buf, size_t len, int flags, co if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } +#ifdef __APPLE__ + std::int64_t result; +#else int result; +#endif if (!to) { if (tolen != 0) return fail(NET_EINVAL); +#ifdef __APPLE__ + result = send_stream_in_chunks(*socket.native, flags, static_cast(buf), len, [&](const char* data, std::size_t size) { + return ::send(socket.native->value, data, size, NATIVE_SEND_FLAGS | native_message_flags(flags)); + }); +#else result = native_transfer(*socket.native, flags, true, [&](int offset) { return ::send(socket.native->value, offset ? static_cast(buf) + offset : static_cast(buf), static_cast(len) - offset, NATIVE_SEND_FLAGS | native_message_flags(flags)); }, true, static_cast(len)); +#endif } else { sockaddr_storage destination{}; NativeLength destination_length = 0; if (!guest_to_native_address(to, tolen, destination, destination_length)) return fail(NET_EINVAL); if (destination.ss_family != (socket.family == NET_AF_INET ? AF_INET : AF_INET6)) return fail(NET_EAFNOSUPPORT); +#ifdef __APPLE__ + result = send_stream_in_chunks(*socket.native, flags, static_cast(buf), len, [&](const char* data, std::size_t size) { + return ::sendto(socket.native->value, data, size, NATIVE_SEND_FLAGS | native_message_flags(flags), + reinterpret_cast(&destination), destination_length); + }); +#else result = native_transfer(*socket.native, flags, true, [&](int offset) { return ::sendto(socket.native->value, offset ? static_cast(buf) + offset : static_cast(buf), static_cast(len) - offset, NATIVE_SEND_FLAGS | native_message_flags(flags), reinterpret_cast(&destination), destination_length); }, true, static_cast(len)); +#endif } if (result < 0) return fail(native_error()); #if defined(_WIN32) || defined(__APPLE__) @@ -1347,7 +1430,13 @@ int APS5_VABI sceNetShutdown(int s, int how) { if (it != g_socks.end() && it->second.native == socket.native) it->second.read_shut = true; g_cv.NotifyAll(); } - return error == 0 ? 0 : fail(error); + if (error != 0) return fail(error); + if (how != 0) { + std::lock_guard lk(g_mutex); + const auto it = g_socks.find(s); + if (it != g_socks.end() && it->second.native == socket.native) it->second.sending_shut = true; + } + return 0; } int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval, uint32_t optlen) { @@ -1385,7 +1474,12 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval if (!optval || optlen < sizeof(int)) return fail(NET_EINVAL); int value = 0; std::memcpy(&value, optval, sizeof(value)); - if (optname == NET_SO_NBIO) { + if (optname == NET_SO_OOBINLINE) { + const int enabled = value != 0; + if (socket.type == NET_SOCK_STREAM && + ::setsockopt(socket.native->value, SOL_SOCKET, SO_OOBINLINE, reinterpret_cast(&enabled), sizeof(enabled)) != 0) + return fail(native_error()); + } else if (optname == NET_SO_NBIO) { #ifndef _WIN32 const int old_flags = fcntl(socket.native->value, F_GETFL, 0); if (old_flags < 0 || fcntl(socket.native->value, F_SETFL, @@ -1399,6 +1493,11 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval const int native_option = optname == NET_SO_REUSEADDR ? SO_REUSEADDR : optname == NET_SO_BROADCAST ? SO_BROADCAST : optname == NET_SO_SNDBUF ? SO_SNDBUF : SO_RCVBUF; if (::setsockopt(socket.native->value, SOL_SOCKET, native_option, reinterpret_cast(&value), sizeof(value)) != 0) return fail(native_error()); + } else if (optname == NET_SO_KEEPALIVE) { + const int enabled = value != 0; + if (socket.type == NET_SOCK_STREAM && + ::setsockopt(socket.native->value, SOL_SOCKET, SO_KEEPALIVE, reinterpret_cast(&enabled), sizeof(enabled)) != 0) + return fail(native_error()); } else if (optname == NET_SO_ONESBCAST) { } else { return fail(NET_EOPNOTSUPP); @@ -1406,12 +1505,14 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval std::lock_guard lk(g_mutex); auto it = g_socks.find(s); if (it != g_socks.end()) { + if (optname == NET_SO_OOBINLINE && it->second.native == socket.native) it->second.oobInline = value != 0; if (optname == NET_SO_NBIO) it->second.native->nonblock = value != 0; if (optname == NET_SO_RCVTIMEO) it->second.native->receiveTimeout = value; if (optname == NET_SO_SNDTIMEO) it->second.native->sendTimeout = value; if (optname == NET_SO_SNDBUF) it->second.snd_buffer = value; if (optname == NET_SO_RCVBUF) it->second.rcv_buffer = value; if (optname == NET_SO_ONESBCAST) it->second.onesbcast = value; + if (optname == NET_SO_KEEPALIVE) it->second.keepalive = value != 0 ? NET_SO_KEEPALIVE : 0; } return 0; } @@ -1425,6 +1526,32 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint socket = it->second; } if (optval == nullptr || optlen == nullptr) return fail(NET_EINVAL); + if (level == NET_SOL_SOCKET && (optname == NET_SO_TYPE || optname == NET_SO_ACCEPTCONN)) { + const int value = optname == NET_SO_TYPE ? socket.type : socket.listening ? NET_SO_ACCEPTCONN : 0; + *optlen = std::min(*optlen, static_cast(sizeof(value))); + std::memcpy(optval, &value, *optlen); + return 0; + } + if (level == NET_SOL_SOCKET && optname == NET_SO_OOBINLINE) { + int enabled = 0; + if (socket.type == NET_SOCK_STREAM) { + NativeLength size = sizeof(enabled); + if (::getsockopt(socket.native->value, SOL_SOCKET, SO_OOBINLINE, reinterpret_cast(&enabled), &size) != 0) + return fail(native_error()); + } else { + enabled = socket.oobInline; + } + const int value = enabled != 0 ? NET_SO_OOBINLINE : 0; + *optlen = std::min(*optlen, static_cast(sizeof(value))); + std::memcpy(optval, &value, *optlen); + return 0; + } + if (level == NET_SOL_SOCKET && optname == NET_SO_PROTOCOL) { + if (socket.type == NET_SOCK_RAW) NotImplemented_nid_no_patch(__func__); + *optlen = std::min(*optlen, static_cast(sizeof(socket.protocol))); + std::memcpy(optval, &socket.protocol, *optlen); + return 0; + } if (level == NET_SOL_SOCKET && (optname == NET_SO_RCVTIMEO || optname == NET_SO_SNDTIMEO)) { const int microseconds = optname == NET_SO_RCVTIMEO ? socket.native->receiveTimeout.load() : socket.native->sendTimeout.load(); const KernelTimeval timeout{microseconds / 1000000, microseconds % 1000000}; @@ -1432,7 +1559,18 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint std::memcpy(optval, &timeout, *optlen); return 0; } - if (*optlen < sizeof(int)) return fail(NET_EINVAL); + if (level == NET_SOL_SOCKET && optname == NET_SO_KEEPALIVE) { + int enabled = socket.keepalive; + if (socket.type == NET_SOCK_STREAM) { + NativeLength size = sizeof(enabled); + if (::getsockopt(socket.native->value, SOL_SOCKET, SO_KEEPALIVE, reinterpret_cast(&enabled), &size) != 0) + return fail(native_error()); + } + const int value = enabled != 0 ? NET_SO_KEEPALIVE : 0; + *optlen = std::min(*optlen, static_cast(sizeof(value))); + std::memcpy(optval, &value, *optlen); + return 0; + } if (level == NET_IPPROTO_TCP && socket.type != NET_SOCK_STREAM) return fail(NET_EINVAL); if (level == NET_IPPROTO_TCP && optname == NET_TCP_NODELAY) { int enabled = 0; @@ -1440,14 +1578,26 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint if (::getsockopt(socket.native->value, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&enabled), &size) != 0) return fail(native_error()); const int value = enabled != 0 ? NET_TF_NODELAY : 0; - std::memcpy(optval, &value, sizeof(value)); - *optlen = sizeof(value); + *optlen = std::min(*optlen, static_cast(sizeof(value))); + std::memcpy(optval, &value, *optlen); return 0; } + if (*optlen < sizeof(int) && + !(level == NET_SOL_SOCKET && (optname == NET_SO_REUSEADDR || optname == NET_SO_BROADCAST))) return fail(NET_EINVAL); if (level == NET_IPPROTO_TCP) return fail(NET_ENOPROTOOPT); if (level != NET_SOL_SOCKET) return fail(NET_EOPNOTSUPP); int value = 0; switch (timeout_option(optname)) { + case NET_SO_REUSEADDR: + case NET_SO_BROADCAST: { + int enabled = 0; + NativeLength size = sizeof(enabled); + const int nativeOption = optname == NET_SO_REUSEADDR ? SO_REUSEADDR : SO_BROADCAST; + if (::getsockopt(socket.native->value, SOL_SOCKET, nativeOption, reinterpret_cast(&enabled), &size) != 0) + return fail(native_error()); + value = enabled != 0 ? optname : 0; + break; + } case NET_SO_NBIO: value = socket.native->nonblock ? 1 : 0; break; case NET_SO_RCVTIMEO: value = socket.native->receiveTimeout; break; case NET_SO_SNDTIMEO: value = socket.native->sendTimeout; break; @@ -1479,8 +1629,8 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint } default: return fail(NET_EOPNOTSUPP); } - std::memcpy(optval, &value, sizeof(value)); - *optlen = sizeof(value); + *optlen = std::min(*optlen, static_cast(sizeof(value))); + std::memcpy(optval, &value, *optlen); return 0; } @@ -1723,8 +1873,7 @@ int APS5_VABI sceNetGetSockInfo6(std::int32_t s, NetSockInfo6* info, std::uint32 } int APS5_VABI sceNetEpollCreate(const char* name, int flags) { - (void)name; - (void)flags; + if (!name || flags != 0) return fail(NET_EINVAL); std::lock_guard lk(g_mutex); const int id = g_next_epoll++; g_epolls.insert(id); @@ -1814,7 +1963,7 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int struct Watch { int descriptor; std::shared_ptr registration; - std::shared_ptr native; + Sock socket; }; const auto deadline = timeout > 0 ? std::chrono::steady_clock::now() + std::chrono::microseconds(timeout) @@ -1830,7 +1979,7 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int for (const auto& entry : registration->second) { const auto socket = g_socks.find(entry.first); if (socket == g_socks.end()) continue; - watches.push_back({entry.first, entry.second, socket->second.native}); + watches.push_back({entry.first, entry.second, socket->second}); } } } @@ -1848,14 +1997,14 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int #ifdef _WIN32 std::vector descriptors(watches.size()); for (std::size_t index = 0; index < descriptors.size(); ++index) { - descriptors[index].fd = watches[index].native->value; + descriptors[index].fd = watches[index].socket.native->value; if (watches[index].registration->events & 1) descriptors[index].events |= POLLRDNORM; if (watches[index].registration->events & 2) descriptors[index].events |= POLLWRNORM; } #else std::vector descriptors(watches.size()); for (std::size_t index = 0; index < descriptors.size(); ++index) { - descriptors[index].fd = watches[index].native->value; + descriptors[index].fd = watches[index].socket.native->value; if (watches[index].registration->events & 1) descriptors[index].events |= POLLIN; if (watches[index].registration->events & 2) descriptors[index].events |= POLLOUT; } @@ -1870,6 +2019,8 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int } #ifdef _WIN32 const int ready_count = WSAPoll(descriptors.data(), static_cast(descriptors.size()), wait_ms); + for (std::size_t index = 0; index < descriptors.size(); ++index) + descriptors[index].revents = closed_stream_events(watches[index].socket, descriptors[index].revents, descriptors[index].events); #else const int ready_count = ::poll(descriptors.data(), descriptors.size(), wait_ms); #endif @@ -1942,7 +2093,7 @@ int APS5_VABI sceNetInetPton(int af, const char* src, void* dst) { return 0; } const std::string text = af == NET_AF_INET || dottedTail ? head : std::string(src); - const int converted = convert(af == NET_AF_INET ? AF_INET : AF_INET6, text, address.data()); + const int converted = af == NET_AF_INET6 ? __inet_pton_nid_postfix(af, text.c_str(), address.data()) : convert(AF_INET, text, address.data()); if (converted == 1 && dst != nullptr) { std::memcpy(dst, address.data(), af == NET_AF_INET ? 4 : 16); } @@ -1960,9 +2111,9 @@ const char* APS5_VABI sceNetInetNtop(int af, const void* src, char* dst, uint32_ } char text[INET6_ADDRSTRLEN]; #ifdef _WIN32 - const auto* result = InetNtopA(af == NET_AF_INET ? AF_INET : AF_INET6, const_cast(src), text, sizeof(text)); + const auto* result = af == NET_AF_INET6 ? __inet_ntop_nid_postfix(af, src, text, sizeof(text)) : InetNtopA(AF_INET, const_cast(src), text, sizeof(text)); #else - const auto* result = ::inet_ntop(af == NET_AF_INET ? AF_INET : AF_INET6, src, text, sizeof(text)); + const auto* result = af == NET_AF_INET6 ? __inet_ntop_nid_postfix(af, src, text, sizeof(text)) : ::inet_ntop(AF_INET, src, text, sizeof(text)); #endif if (!result) { set_errno(native_error()); @@ -2008,8 +2159,7 @@ int APS5_VABI sceNetEtherStrton(const char* text, NetEtherAddr* address) { } int APS5_VABI sceNetGetMacAddress(NetEtherAddr* addr, int flags) { - (void)flags; - if (addr == nullptr) { + if (addr == nullptr || flags != 0) { return fail(NET_EINVAL); } // Keep guest identity stable without exposing the host adapter address. @@ -2022,7 +2172,7 @@ int APS5_VABI sceNetGetMacAddress(NetEtherAddr* addr, int flags) { int APS5_VABI sceNetResolverCreate(const char* name, int memid, int flags) { (void)name; (void)memid; - (void)flags; + if (flags != 0) return fail(NET_EINVAL); std::lock_guard lk(g_mutex); const int id = g_next_resolver++; g_resolvers[id] = {}; @@ -2082,9 +2232,7 @@ int lookup_ipv4(int rid, const char* hostname, const char* func, std::vector(flags) & 0xfffefffeu) != 0) return fail(NET_EINVAL); if (!hostname || !addr) return fail(NET_EINVAL); std::vector addresses; if (const int error = lookup_ipv4(rid, hostname, __func__, addresses)) return error; @@ -2225,7 +2373,14 @@ int APS5_VABI sceNetGetInterfaceStats(NetInterfaceStats* stats, std::uint32_t fl } int APS5_VABI sceNetSetDnsInfo(const NetDnsInfo* info, std::uint32_t flags) { - NotImplemented_nid_no_patch(__func__); + if (!g_net_inited) return fail(NET_ENOTINIT); + if (flags != 0) return fail(NET_EINVAL); + NetDnsInfo servers{}; + if (info && !GuestSocketReadGuest_nid_no_patch(&servers, info, sizeof(servers))) return fail(NET_EFAULT); + if (info && servers.primaryAddress == 0) return fail(NET_EINVAL); + std::lock_guard lock(g_dnsLock); + g_dnsOverridden = info != nullptr; + g_dnsOverride = servers; return 0; } @@ -2274,7 +2429,13 @@ int APS5_VABI sceNetResolverConnectDestroy(NetResolverConnection* connection) { } int APS5_VABI sceNetSetDns6Info(const NetDns6Info* info, std::uint32_t flags) { - NotImplemented_nid_no_patch(__func__); + if (!g_net_inited) return fail(NET_ENOTINIT); + if (flags != 0) return fail(NET_EINVAL); + NetDns6Info servers{}; + if (info && !GuestSocketReadGuest_nid_no_patch(&servers, info, sizeof(servers))) return fail(NET_EFAULT); + if (info && IsZeroAddress(servers.primaryAddress)) return fail(NET_EINVAL); + std::lock_guard lock(g_dnsLock); + g_dns6Override = servers; return 0; } @@ -2343,8 +2504,15 @@ int APS5_VABI sceNetGetAppMemInfo(NetAppMemoryInfo* info, std::uint32_t flags) { } int APS5_VABI sceNetGetDnsInfo(NetDnsInfo* info, std::uint32_t flags) { - if (flags != 0) throw std::runtime_error("sceNetGetDnsInfo: flags " + std::to_string(flags) + " are not supported"); - if (!info) throw std::runtime_error("sceNetGetDnsInfo: a null info is not supported"); + if (!g_net_inited) return fail(NET_ENOTINIT); + if (flags != 0 || !info) return fail(NET_EINVAL); + { + std::lock_guard lock(g_dnsLock); + if (g_dnsOverridden) { + *info = g_dnsOverride; + return info->secondaryAddress == 0 ? 1 : 2; + } + } std::array servers{}; const std::size_t count = std::min(NetCtlHostDnsServers_nid_no_patch(servers.data(), servers.size()), servers.size()); std::memcpy(info, servers.data(), sizeof(servers)); @@ -2554,7 +2722,11 @@ int APS5_VABI sceNetThreadJoin(void* thread) { int APS5_VABI sceNetUsleep(int microseconds) { if (!g_net_inited) return fail(NET_ENOTINIT); - if (microseconds < 0) return fail(NET_EINVAL); + if (microseconds < 0) { + std::fprintf(stdout, "[LIBNET] usleep %d\n", microseconds); + std::fflush(stdout); + return fail(NET_EINVAL); + } const int result = sceKernelUsleep_nid_postfix(static_cast(microseconds)); if (result < 0) set_errno((result & 0xFF00) == 0x100 ? result & 0xFF : 205); return result; @@ -2596,8 +2768,12 @@ int APS5_VABI sceNetInetPtonWithScopeId(std::int32_t family, const char* text, v } int APS5_VABI sceNetGetDns6Info(NetDns6Info* info, std::uint32_t flags) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (!g_net_inited) return fail(NET_ENOTINIT); + if (flags != 0 || !info) return fail(NET_EINVAL); + std::lock_guard lock(g_dnsLock); + *info = g_dns6Override; + if (IsZeroAddress(info->primaryAddress)) return 0; + return IsZeroAddress(info->secondaryAddress) ? 1 : 2; } int APS5_VABI sceNetSetDns6InfoToKernel(const NetDns6Info* info, std::uint32_t flags) { diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index d3e153e34a..a5e469dfdd 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -637,8 +637,7 @@ int APS5_VABI sceNetCtlRegisterCallbackForNpToolkit(NetCtlCallback callback, voi } int APS5_VABI sceNetCtlRegisterCallbackIpcInt(NetCtlCallback callback, void* argument, int* callbackId) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceNetCtlRegisterCallback(callback, argument, callbackId); } int APS5_VABI sceNetCtlRegisterCallbackForLibIpcInt(NetCtlCallback callback, void* argument, int* callbackId) { @@ -651,8 +650,7 @@ int APS5_VABI sceNetCtlUnregisterCallbackForNpToolkit(int callbackId) { } int APS5_VABI sceNetCtlUnregisterCallbackIpcInt(int callbackId) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceNetCtlUnregisterCallback(callbackId); } int APS5_VABI sceNetCtlUnregisterCallbackForLibIpcInt(int callbackId) { @@ -685,13 +683,11 @@ int APS5_VABI sceNetCtlClearEventForLibIpcInt(int event) { } int APS5_VABI sceNetCtlGetResultIpcInt(int event, int* result) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceNetCtlGetResult(event, result); } int APS5_VABI sceNetCtlGetStateIpcInt(int* state) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceNetCtlGetState(state); } int APS5_VABI sceNetCtlGetState2IpcInt(int* state) { @@ -700,8 +696,7 @@ int APS5_VABI sceNetCtlGetState2IpcInt(int* state) { } int APS5_VABI sceNetCtlGetInfoIpcInt(std::uint32_t code, NetCtlInfo* info) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceNetCtlGetInfo(static_cast(code), info); } int APS5_VABI sceNetCtlGetEtherLinkMode(int* mode) { @@ -770,13 +765,11 @@ int APS5_VABI sceNetCtlSetErrorNotificationEnabledIpcInt(int enabled) { } int APS5_VABI sceNetCtlRegisterCallbackV6IpcInt(NetCtlCallback callback, void* argument, int* callbackId) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceNetCtlRegisterCallbackV6(callback, argument, callbackId); } int APS5_VABI sceNetCtlUnregisterCallbackV6IpcInt(int callbackId) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceNetCtlUnregisterCallbackV6(callbackId); } int APS5_VABI sceNetCtlGetResultV6(int event_type, int* error_code) { @@ -789,18 +782,15 @@ int APS5_VABI sceNetCtlGetResultV6(int event_type, int* error_code) { } int APS5_VABI sceNetCtlGetResultV6IpcInt(int event, int* result) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceNetCtlGetResultV6(event, result); } int APS5_VABI sceNetCtlGetStateV6IpcInt(int* state) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceNetCtlGetStateV6(state); } int APS5_VABI sceNetCtlGetInfoV6IpcInt(std::uint32_t code, NetCtlInfoV6* info) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceNetCtlGetInfoV6(code, info); } int APS5_VABI sceNetCtlGetNatInfoIpcInt(NetCtlNatInfoInternal* info) { diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index 93bb6c445e..885da1168b 100644 --- a/core/libs/prx/libSceNgs2.native/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2.native/CMakeLists.txt @@ -2,7 +2,7 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/Ngs2Library.cmake) add_sce_ngs2_library(libSceNgs2.native) if(BUILD_TESTING) - foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx Report Geom VoiceIndex HandleErrors VoiceHandles) + foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx Report Geom VoiceIndex HandleErrors VoiceHandles VoiceEvents) string(TOLOWER ${ngs2Test} testName) add_test_executable(ngs2_${testName}_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Ngs2${ngs2Test}.cpp) target_include_directories(ngs2_${testName}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) @@ -14,4 +14,5 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_sce_ngs2_test(libSceNgs2.native ngs2_pcm Ngs2Pcm.cpp) endif() diff --git a/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake index b89226f992..27dde605fe 100644 --- a/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake +++ b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake @@ -5,6 +5,7 @@ function(add_sce_ngs2_library target) ${ngs2Dir}/src/Custom.cpp ${ngs2Dir}/src/Geom.cpp ${ngs2Dir}/src/Pan.cpp + ${ngs2Dir}/src/Pcm.cpp ${ngs2Dir}/src/Rack.cpp ${ngs2Dir}/src/Render.cpp ${ngs2Dir}/src/Reverb.cpp @@ -23,3 +24,15 @@ function(add_sce_ngs2_library target) ) configure_windows_unwind(${target}) endfunction() + +function(add_sce_ngs2_test library name source) + add_test_executable(${name}_tests ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/tests/${source}) + target_include_directories(${name}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) + target_link_libraries(${name}_tests PRIVATE ${library} atrac9 libkernel libc) + configure_windows_unwind(${name}_tests) + add_test(NAME ${name} COMMAND ${name}_tests) + if(WIN32) + set_tests_properties(${name} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endfunction() diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index eda5e07ef3..9f0abe430f 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -25,7 +25,11 @@ static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT = static_cast(0 static constexpr int SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT = static_cast(0x804A8432); static constexpr int SCE_NGS2_ERROR_INVALID_PAN_UNIT_ANGLE = static_cast(0x804A8450); static constexpr int SCE_NGS2_ERROR_INVALID_PAN_SPEAKER = static_cast(0x804A8451); +static constexpr int SCE_NGS2_ERROR_INVALID_PAN_MATRIX_FORMAT = static_cast(0x804A8452); +static constexpr int SCE_NGS2_ERROR_INVALID_PAN_WORK = static_cast(0x804A8453); +static constexpr int SCE_NGS2_ERROR_INVALID_PAN_PARAM = static_cast(0x804A8454); static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_HANDLE = static_cast(0x804A8203); +static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_ACCESS = static_cast(0x804A8351); static constexpr int SCE_NGS2_ERROR_NULL_PARAM_LIST = static_cast(0x804A8360); static constexpr int SCE_NGS2_ERROR_INVALID_GEOM_LISTENER_ADDRESS = static_cast(0x804A8461); static constexpr int SCE_NGS2_ERROR_INVALID_GEOM_FLAG = static_cast(0x804A8463); @@ -45,10 +49,14 @@ static constexpr std::uint32_t SCE_NGS2_RACK_ID_CUSTOM_SAMPLER = 0x4001; static constexpr std::uint32_t SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER = 0x4002; static constexpr std::uint32_t SCE_NGS2_RACK_ID_CUSTOM_MASTERING = 0x4003; +static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_U8 = 0x11; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_I16L = 0x12; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_I16B = 0x13; +static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_I24L = 0x14; +static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_I32L = 0x16; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_F32L = 0x18; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_F32B = 0x19; +static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_F64L = 0x1a; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_ATRAC9 = 0x40; static constexpr std::uint32_t SCE_NGS2_VOICE_EVENT_PLAY = 1; diff --git a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp index ecebdfed40..dda42ea297 100644 --- a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp @@ -17,8 +17,10 @@ #include "Ngs2Internal.hpp" static constexpr std::uint16_t WAVE_FORMAT_PCM = 0x1; +static constexpr std::uint16_t WAVE_FORMAT_IEEE_FLOAT = 0x3; static constexpr std::uint16_t WAVE_FORMAT_EXTENSIBLE = 0xfffe; static constexpr std::uint8_t ATRAC9_GUID[16] = {0xd2, 0x42, 0xe1, 0x47, 0xba, 0x36, 0x8d, 0x4d, 0x88, 0xfc, 0x61, 0x65, 0x4f, 0x8c, 0x83, 0x6c}; +static constexpr std::uint8_t PCM_GUID[16] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}; static constexpr std::size_t FMT_ATRAC9_SIZE = 52; static constexpr std::size_t FMT_GUID_OFFSET = 24; static constexpr std::size_t FMT_CONFIG_OFFSET = 44; @@ -363,19 +365,27 @@ static int ParseAtrac9(const RiffChunks& chunks, Ngs2WaveformInfo& info) { return ParseAtrac9Loop(chunks, info); } -static int ParsePcm16(const RiffChunks& chunks, std::size_t size, Ngs2WaveformInfo& info) { +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; +} + +static int ParsePcm(const RiffChunks& chunks, std::size_t size, Ngs2WaveformInfo& info, bool floating) { const auto* format = chunks.format; if (chunks.formatSize < 16) return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; + const std::uint32_t bits = ReadLe16(format + 14); + const std::uint32_t type = PcmWaveformType(bits, floating); + if (type == 0) return SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT; const std::uint32_t channels = ReadLe16(format + 2); const std::uint32_t sampleRate = ReadLe32(format + 4); - const std::uint32_t bytesPerFrame = channels * sizeof(std::int16_t); - if (ReadLe16(format + 14) != 16 || channels == 0 || channels > NGS2_MAX_CHANNELS || sampleRate == 0 || sampleRate > MAX_SAMPLE_RATE || ReadLe16(format + 12) != bytesPerFrame) { + const std::uint32_t bytesPerFrame = channels * (bits / 8); + 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 = {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, channels, sampleRate, 0, 0, 0}; + info.format = {type, channels, sampleRate, 0, 0, 0}; info.data_offset = static_cast(chunks.dataOffset); info.data_size = static_cast(dataSize); info.num_samples = static_cast(dataSize / bytesPerFrame); @@ -398,7 +408,12 @@ static int ParseWaveform(const RiffChunks& chunks, std::size_t size, Ngs2Wavefor std::memcmp(chunks.format + FMT_GUID_OFFSET, ATRAC9_GUID, sizeof(ATRAC9_GUID)) == 0) { return ParseAtrac9(chunks, info); } - if (tag == WAVE_FORMAT_PCM) return ParsePcm16(chunks, size, info); + if (tag == WAVE_FORMAT_EXTENSIBLE && chunks.formatSize >= FMT_GUID_OFFSET + sizeof(PCM_GUID) && + std::memcmp(chunks.format + FMT_GUID_OFFSET, PCM_GUID, sizeof(PCM_GUID)) == 0) { + return ParsePcm(chunks, size, info, false); + } + if (tag == WAVE_FORMAT_PCM) return ParsePcm(chunks, size, info, false); + if (tag == WAVE_FORMAT_IEEE_FLOAT) return ParsePcm(chunks, size, info, true); throw std::runtime_error("NGS2: parsing waveform format tag " + Ngs2Hex(tag) + " is not implemented"); } diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index 73270afab9..b5a9935162 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -191,6 +191,8 @@ struct Ngs2System { }; std::string Ngs2Hex(std::uint32_t value); +std::size_t Ngs2PcmSampleBytes(std::uint32_t waveformType); +void Ngs2ReadPcmFrame(std::uint32_t waveformType, std::uint32_t channels, const std::uint8_t* frame, float* output); const std::uint8_t* Ngs2StreamEnd(const Ngs2Voice& voice, const Ngs2Block& block); std::recursive_mutex& Ngs2Mutex(); diff --git a/core/libs/prx/libSceNgs2.native/src/Pan.cpp b/core/libs/prx/libSceNgs2.native/src/Pan.cpp index e1a4d70548..f59c436bc9 100644 --- a/core/libs/prx/libSceNgs2.native/src/Pan.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Pan.cpp @@ -1,11 +1,9 @@ -#include #include #include #include #include #include #include -#include #include "prx/libc/include/General.hpp" #include "Ngs2Internal.hpp" @@ -57,14 +55,16 @@ static void AddBetweenSpeakers(const Ngs2PanWork& work, std::uint32_t speaker, f int Ngs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle, uint32_t num_speakers) { if (work == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; if (speaker_angles != nullptr && !IsUnitAngle(unit_angle)) return SCE_NGS2_ERROR_INVALID_PAN_UNIT_ANGLE; - const auto* layout = LayoutFor(num_speakers); - if (layout == nullptr) { - if (speaker_angles == nullptr && num_speakers == 0) APS5_INVALID_ARG_EX; - return SCE_NGS2_ERROR_INVALID_PAN_SPEAKER; + if (speaker_angles == nullptr && num_speakers == 0) { + Ngs2PanInit(work, nullptr, DEGREES, 7); + work->unit_angle = unit_angle == RADIANS ? RADIANS : DEGREES; + return SCE_NGS2_OK; } - *work = {}; - work->num_speakers = num_speakers; + const auto* layout = LayoutFor(num_speakers); + if (layout == nullptr) return SCE_NGS2_ERROR_INVALID_PAN_SPEAKER; if (speaker_angles == nullptr) { + *work = {}; + work->num_speakers = num_speakers; for (std::uint32_t i = 0; i < num_speakers; i++) work->speaker_angles[i] = layout->defaultDegrees[i]; work->speaker_angles[num_speakers] = layout->defaultDegrees[0] + DEGREES; work->unit_angle = DEGREES; @@ -76,39 +76,39 @@ int Ngs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle } for (std::uint32_t i = 0; i < num_speakers; i++) { float angle = speaker_angles[layout->inputOrder[i]]; - if (!std::isfinite(angle)) APS5_INVALID_ARG_EX; while (angle < 0.0f) { - if (angle + unit_angle == angle) throw std::runtime_error("NGS2: pan angles too large to wrap are not implemented"); + if (angle + unit_angle == angle) throw std::runtime_error("NGS2: pan speaker angles too large to wrap are not implemented (the PS5 library did not return for them within 60 s)"); angle += unit_angle; } while (angle >= unit_angle) { - if (!(unit_angle < angle)) APS5_INVALID_ARG_EX; - if (angle - unit_angle == angle) throw std::runtime_error("NGS2: pan angles too large to wrap are not implemented"); + if (!(unit_angle < angle)) throw std::runtime_error("NGS2: a pan speaker angle that wraps to the unit angle is not implemented (the PS5 library did not return for it within 60 s)"); + if (angle - unit_angle == angle) throw std::runtime_error("NGS2: pan speaker angles too large to wrap are not implemented (the PS5 library did not return for them within 60 s)"); angle -= unit_angle; } work->speaker_angles[i] = angle; } work->speaker_angles[num_speakers] = work->speaker_angles[0] + unit_angle; work->unit_angle = unit_angle; + work->num_speakers = num_speakers; return SCE_NGS2_OK; } int Ngs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* params, uint32_t num_params, uint32_t matrix_format, float* out_volume_matrix) { - if (work == nullptr || params == nullptr) APS5_INVALID_ARG_EX; + if (work == nullptr) return SCE_NGS2_ERROR_INVALID_PAN_WORK; + if (params == nullptr) return SCE_NGS2_ERROR_INVALID_PAN_PARAM; if (out_volume_matrix == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; - if (matrix_format != 1 && matrix_format != 2 && matrix_format != 6 && matrix_format != 8) APS5_INVALID_ARG_EX; + if (matrix_format != 1 && matrix_format != 2 && matrix_format != 6 && matrix_format != 8) return SCE_NGS2_ERROR_INVALID_PAN_MATRIX_FORMAT; const auto* layout = LayoutFor(work->num_speakers); - if (layout == nullptr || !IsUnitAngle(work->unit_angle) || !std::isfinite(work->speaker_angles[0]) || num_params > MAX_PAN_PARAMS) APS5_INVALID_ARG_EX; - for (std::uint32_t p = 0; p < num_params; p++) { - if (!std::isfinite(params[p].angle) || std::isnan(params[p].distance)) APS5_INVALID_ARG_EX; - } + if (layout == nullptr) return SCE_NGS2_ERROR_INVALID_PAN_SPEAKER; + if (!IsUnitAngle(work->unit_angle)) return SCE_NGS2_ERROR_INVALID_PAN_UNIT_ANGLE; + if (num_params > MAX_PAN_PARAMS) return SCE_NGS2_ERROR_INVALID_NUM_CHANNELS; std::memset(out_volume_matrix, 0, static_cast(num_params) * matrix_format * sizeof(float)); const std::uint32_t numSpeakers = work->num_speakers; const float unit = work->unit_angle; const float first = work->speaker_angles[0]; for (std::uint32_t p = 0; p < num_params; p++) { const auto& param = params[p]; - const float distance = std::max(-1.0f, std::min(1.0f, param.distance)); + const float distance = param.distance > 1.0f ? 1.0f : param.distance < -1.0f ? -1.0f : param.distance; const float towardWeight = std::sin((distance + 1.0f) * 0.5f * std::numbers::pi_v * 0.5f); const float oppositeWeight = std::sin((1.0f - distance) * 0.5f * std::numbers::pi_v * 0.5f); float angle = param.angle; @@ -116,7 +116,7 @@ int Ngs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* params, uint32 const float before = angle; if (angle >= first + unit) angle -= unit; if (angle < first) angle += unit; - if (angle == before) throw std::runtime_error("NGS2: pan angles too large to wrap are not implemented"); + if (angle == before) throw std::runtime_error("NGS2: pan angles that do not wrap into one turn are not implemented (the PS5 library did not return for them within 60 s)"); } float opposite = angle + unit * 0.5f; if (!(opposite < first + unit)) opposite -= unit; diff --git a/core/libs/prx/libSceNgs2.native/src/Pcm.cpp b/core/libs/prx/libSceNgs2.native/src/Pcm.cpp new file mode 100644 index 0000000000..49b8611046 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/src/Pcm.cpp @@ -0,0 +1,57 @@ +#include +#include +#include + +#include "Ngs2Internal.hpp" + +std::size_t Ngs2PcmSampleBytes(std::uint32_t waveformType) { + switch (waveformType) { + case SCE_NGS2_WAVEFORM_TYPE_PCM_U8: return 1; + case SCE_NGS2_WAVEFORM_TYPE_PCM_I16L: return 2; + case SCE_NGS2_WAVEFORM_TYPE_PCM_I24L: return 3; + case SCE_NGS2_WAVEFORM_TYPE_PCM_I32L: + case SCE_NGS2_WAVEFORM_TYPE_PCM_F32L: return 4; + case SCE_NGS2_WAVEFORM_TYPE_PCM_F64L: return 8; + default: throw std::runtime_error("NGS2: PCM waveform type " + Ngs2Hex(waveformType) + " is not implemented"); + } +} + +static std::uint64_t ReadLittleEndian(const std::uint8_t* data, std::size_t bytes) { + std::uint64_t value = 0; + for (std::size_t i = 0; i < bytes; i++) value |= static_cast(data[i]) << (i * 8); + return value; +} + +static void CheckFloatRange(std::uint64_t magnitude, std::uint64_t minimum, std::uint64_t maximum) { + if (magnitude > maximum || (magnitude != 0 && magnitude < minimum)) { + throw std::runtime_error("NGS2: nonfinite, subnormal or out-of-range floating-point PCM is not implemented"); + } +} + +static float ReadSample(std::uint32_t waveformType, const std::uint8_t* sample, std::size_t bytes) { + const auto value = ReadLittleEndian(sample, bytes); + switch (waveformType) { + case SCE_NGS2_WAVEFORM_TYPE_PCM_U8: + return (static_cast(value) - 128) / 128.0f; + case SCE_NGS2_WAVEFORM_TYPE_PCM_I16L: + return std::bit_cast(static_cast(value)) / 32768.0f; + case SCE_NGS2_WAVEFORM_TYPE_PCM_I24L: + return (static_cast(value ^ 0x800000) - 0x800000) / 8388608.0f; + case SCE_NGS2_WAVEFORM_TYPE_PCM_I32L: + return static_cast(std::bit_cast(static_cast(value)) / 2147483648.0); + case SCE_NGS2_WAVEFORM_TYPE_PCM_F32L: + CheckFloatRange(value & 0x7fffffff, 0x00800000, 0x3f800000); + return std::bit_cast(static_cast(value)); + case SCE_NGS2_WAVEFORM_TYPE_PCM_F64L: + CheckFloatRange(value & 0x7fffffffffffffff, 0x3810000000000000, 0x3ff0000000000000); + return static_cast(std::bit_cast(value)); + default: throw std::runtime_error("NGS2: PCM sample conversion is not implemented"); + } +} + +void Ngs2ReadPcmFrame(std::uint32_t waveformType, std::uint32_t channels, const std::uint8_t* frame, float* output) { + const auto bytes = Ngs2PcmSampleBytes(waveformType); + for (std::uint32_t channel = 0; channel < channels; channel++) { + output[channel] = frame == nullptr ? 0.0f : ReadSample(waveformType, frame + channel * bytes, bytes); + } +} diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index 3fb3689e23..a56dc7072b 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -15,7 +15,7 @@ static constexpr double PHASE_ONE = 4294967296.0; static constexpr std::uint64_t PHASE_FRACTION = 0xffffffff; static const std::uint8_t* FrameAt(const Ngs2Voice& voice, const Ngs2Block& block, std::uint32_t frame) { - const std::size_t frameBytes = voice.channels * sizeof(std::int16_t); + const std::size_t frameBytes = voice.channels * Ngs2PcmSampleBytes(voice.waveformType); if (block.data == nullptr) return nullptr; if (block.streaming) { for (const auto& piece : block.pieces) { @@ -27,15 +27,7 @@ static const std::uint8_t* FrameAt(const Ngs2Voice& voice, const Ngs2Block& bloc } static void ReadPcm(const Ngs2Voice& voice, const std::uint8_t* frame, float* out) { - if (frame == nullptr) { - for (std::uint32_t channel = 0; channel < voice.channels; channel++) out[channel] = 0.0f; - return; - } - for (std::uint32_t channel = 0; channel < voice.channels; channel++) { - std::int16_t sample; - std::memcpy(&sample, frame + channel * sizeof(sample), sizeof(sample)); - out[channel] = sample / 32768.0f; - } + Ngs2ReadPcmFrame(voice.waveformType, voice.channels, frame, out); } static void ReadFrames(Ngs2Voice& voice, float* current, float* next) { @@ -93,7 +85,7 @@ static void Advance(Ngs2Voice& voice, std::uint64_t frames) { const auto step = std::min(frames, limit - block.cursor); block.cursor += static_cast(step); voice.decodedSamples += step; - if (voice.waveformType != SCE_NGS2_WAVEFORM_TYPE_ATRAC9) voice.decodedBytes += step * voice.channels * sizeof(std::int16_t); + if (voice.waveformType != SCE_NGS2_WAVEFORM_TYPE_ATRAC9) voice.decodedBytes += step * voice.channels * Ngs2PcmSampleBytes(voice.waveformType); frames -= step; if (block.streaming) { while (block.pieces.size() > 1 && block.cursor >= block.pieces.front().firstFrame + block.pieces.front().frames) block.pieces.erase(block.pieces.begin()); @@ -299,6 +291,7 @@ static void WriteOutput(const Ngs2RenderBufferInfo& output, const std::vector(output.buffer); for (std::size_t i = 0; i < mix.size(); i++) { + if (std::isnan(mix[i])) throw std::runtime_error("NGS2: a NaN sample in a PCM_I16L render buffer is not implemented"); pcm[i] = static_cast(std::min(std::lrint(std::clamp(mix[i], -1.0f, 1.0f) * 32768.0f), 32767L)); } } diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index 2b02256918..0efa7461ca 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -20,6 +20,16 @@ static constexpr std::uint8_t COMMAND_TYPE_INT = 3; static constexpr std::uint8_t COMMAND_TYPE_UINT = 4; static constexpr std::uint8_t COMMAND_TYPE_FLOAT_ARRAY = 0x11; static constexpr float MAX_MATRIX_LEVEL = 4.0f; +static constexpr float MAX_MASTERING_GAIN = 32.0f; + +struct Ngs2ReturnCode { + int code; +}; + +static void SendEvent(Ngs2Voice& voice, std::uint32_t eventId) { + if (eventId == SCE_NGS2_VOICE_EVENT_PLAY && voice.channels == 0) throw Ngs2ReturnCode{SCE_NGS2_ERROR_INVALID_VOICE_ACCESS}; + voice.SetEvent(eventId); +} void Ngs2Voice::SetEvent(std::uint32_t eventId) { switch (eventId) { @@ -78,7 +88,7 @@ void Ngs2Voice::ResetSetup() { const std::uint8_t* Ngs2StreamEnd(const Ngs2Voice& voice, const Ngs2Block& block) { const auto& last = block.pieces.back(); - return last.data + last.frames * voice.channels * sizeof(std::int16_t); + return last.data + last.frames * voice.channels * Ngs2PcmSampleBytes(voice.waveformType); } const std::uint8_t* Ngs2Voice::WaveformData() const { @@ -87,11 +97,12 @@ const std::uint8_t* Ngs2Voice::WaveformData() const { if (waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9) return block.data + block.dataCursor; if (block.streaming) { for (const auto& piece : block.pieces) { - if (block.cursor >= piece.firstFrame && block.cursor - piece.firstFrame < piece.frames) return piece.data + (block.cursor - piece.firstFrame) * channels * sizeof(std::int16_t); + if (block.cursor >= piece.firstFrame && block.cursor - piece.firstFrame < piece.frames) return piece.data + (block.cursor - piece.firstFrame) * channels * Ngs2PcmSampleBytes(waveformType); } return Ngs2StreamEnd(*this, block); } - return block.data + (static_cast(block.info.num_skip_samples) + block.cursor) * channels * sizeof(std::int16_t); + if (block.data == nullptr) return nullptr; + return block.data + (static_cast(block.info.num_skip_samples) + block.cursor) * channels * Ngs2PcmSampleBytes(waveformType); } static Ngs2Port& PortAt(Ngs2Voice& voice, std::uint32_t port) { @@ -160,7 +171,7 @@ static void ApplyCommonParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param break; } case SCE_NGS2_VOICE_PARAM_PATCH: Patch(voice, ParamAs(param)); break; - case SCE_NGS2_VOICE_PARAM_EVENT: voice.SetEvent(ParamAs(param).event_id); break; + case SCE_NGS2_VOICE_PARAM_EVENT: SendEvent(voice, ParamAs(param).event_id); break; case SCE_NGS2_VOICE_PARAM_CALLBACK: { const auto& callback = ParamAs(param); voice.callback = callback.callback; @@ -173,10 +184,14 @@ static void ApplyCommonParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param } static void SetupSampler(Ngs2Voice& voice, const Ngs2WaveformFormat& format) { + if (format.waveform_type == 0 && format.num_channels == 0 && format.sample_rate == 0) { + if (voice.channels == 0) voice.ResetSetup(); + return; + } voice.ResetSetup(); - if (format.waveform_type == 0 && format.num_channels == 0 && format.sample_rate == 0) return; - if (format.waveform_type != SCE_NGS2_WAVEFORM_TYPE_PCM_I16L && format.waveform_type != SCE_NGS2_WAVEFORM_TYPE_ATRAC9) { - throw std::runtime_error("NGS2: waveform type " + Ngs2Hex(format.waveform_type) + " is not implemented"); + if (format.waveform_type != SCE_NGS2_WAVEFORM_TYPE_ATRAC9) Ngs2PcmSampleBytes(format.waveform_type); + if (format.waveform_type != SCE_NGS2_WAVEFORM_TYPE_PCM_I16L && format.waveform_type != SCE_NGS2_WAVEFORM_TYPE_ATRAC9 && format.config_data != 0) { + throw std::runtime_error("NGS2: configured PCM waveforms are not implemented"); } if (format.frame_offset != 0 || format.frame_margin != 0) throw std::runtime_error("NGS2: waveform frame offset and margin are not implemented"); if (format.num_channels == 0 || format.num_channels > NGS2_MAX_CHANNELS || format.sample_rate == 0) APS5_INVALID_ARG_EX; @@ -227,7 +242,7 @@ static void AddWaveformBlocks(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBl voice.waveformEnd = nullptr; if (voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9) Ngs2RestartAtrac9(voice); } - const std::size_t frameBytes = voice.channels * sizeof(std::int16_t); + const std::size_t frameBytes = voice.channels == 0 || voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9 ? 0 : voice.channels * Ngs2PcmSampleBytes(voice.waveformType); if ((param.flags & SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND) != 0) { AppendStreamData(voice, param, frameBytes); voice.acceptsBlocks = (param.flags & SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE) != 0; @@ -239,7 +254,7 @@ static void AddWaveformBlocks(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBl const auto& block = param.blocks[i]; if (block.num_samples == 0 && block.data_size == 0) continue; if (allowsSilence && block.data_size == 0) { - if (voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9) throw std::runtime_error("NGS2: a dataless waveform block under the silence flag is not implemented for ATRAC9 voices"); + if (voice.waveformType != SCE_NGS2_WAVEFORM_TYPE_PCM_I16L) throw std::runtime_error("NGS2: a dataless waveform block under the silence flag is not implemented for this format"); voice.blocks.push_back({nullptr, block}); continue; } @@ -380,9 +395,8 @@ static void ApplyParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { } if (param.id == SCE_NGS2_MASTERING_VOICE_PARAM_GAIN || param.id == SCE_NGS2_CUSTOM_MASTERING_VOICE_PARAM_GAIN) { const auto& gain = ParamAs(param); - if (!std::isfinite(gain.fbw_level) || !std::isfinite(gain.lfe_level)) APS5_INVALID_ARG_EX; - voice.fbwLevel = gain.fbw_level; - voice.lfeLevel = gain.lfe_level; + voice.fbwLevel = std::clamp(gain.fbw_level, -MAX_MASTERING_GAIN, MAX_MASTERING_GAIN); + voice.lfeLevel = std::clamp(gain.lfe_level, -MAX_MASTERING_GAIN, MAX_MASTERING_GAIN); return; } if (param.id == SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT || param.id == SCE_NGS2_CUSTOM_MASTERING_VOICE_PARAM_OUTPUT) { @@ -405,9 +419,13 @@ int APS5_VABI sceNgs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamHe auto* found = Ngs2FindVoice(voice_handle); if (found == nullptr) return SCE_NGS2_ERROR_INVALID_VOICE_HANDLE; auto& voice = *found; - for (const auto* param = param_list;; param = reinterpret_cast(reinterpret_cast(param) + param->next)) { - ApplyParam(voice, *param); - if (param->next == 0) break; + try { + for (const auto* param = param_list;; param = reinterpret_cast(reinterpret_cast(param) + param->next)) { + ApplyParam(voice, *param); + if (param->next == 0) break; + } + } catch (const Ngs2ReturnCode& result) { + return result.code; } return SCE_NGS2_OK; } @@ -418,20 +436,24 @@ int APS5_VABI sceNgs2VoiceRunCommands(uintptr_t voice_handle, const Ngs2VoiceCom if (found == nullptr) return SCE_NGS2_ERROR_INVALID_VOICE_HANDLE; auto& voice = *found; if (commands == nullptr && num_commands != 0) APS5_INVALID_ARG_EX; - for (std::size_t i = 0; i < num_commands; i++) { - const auto& command = commands[i]; - const std::uint32_t index = command.id >> 24; - const std::uint32_t id = command.id & 0xffffff; - const std::uint8_t expectedType = id == COMMAND_EVENT ? COMMAND_TYPE_UINT - : id == COMMAND_MATRIX_LEVELS ? COMMAND_TYPE_FLOAT_ARRAY - : id == COMMAND_PORT_VOLUME ? COMMAND_TYPE_FLOAT - : id == COMMAND_PORT_MATRIX ? COMMAND_TYPE_INT : 0; - if (expectedType == 0) throw std::runtime_error("NGS2: voice command " + Ngs2Hex(command.id) + " is not implemented"); - if (command.type != expectedType) throw std::invalid_argument("NGS2: voice command " + Ngs2Hex(command.id) + " has value type " + Ngs2Hex(command.type)); - if (id == COMMAND_EVENT) voice.SetEvent(command.value.u); - else if (id == COMMAND_MATRIX_LEVELS) SetMatrixLevels(voice, index, command.value.levels, command.count); - else if (id == COMMAND_PORT_VOLUME) PortAt(voice, index).volume = command.value.f; - else SetPortMatrix(voice, index, command.value.i); + try { + for (std::size_t i = 0; i < num_commands; i++) { + const auto& command = commands[i]; + const std::uint32_t index = command.id >> 24; + const std::uint32_t id = command.id & 0xffffff; + const std::uint8_t expectedType = id == COMMAND_EVENT ? COMMAND_TYPE_UINT + : id == COMMAND_MATRIX_LEVELS ? COMMAND_TYPE_FLOAT_ARRAY + : id == COMMAND_PORT_VOLUME ? COMMAND_TYPE_FLOAT + : id == COMMAND_PORT_MATRIX ? COMMAND_TYPE_INT : 0; + if (expectedType == 0) throw std::runtime_error("NGS2: voice command " + Ngs2Hex(command.id) + " is not implemented"); + if (command.type != expectedType) throw std::invalid_argument("NGS2: voice command " + Ngs2Hex(command.id) + " has value type " + Ngs2Hex(command.type)); + if (id == COMMAND_EVENT) SendEvent(voice, command.value.u); + else if (id == COMMAND_MATRIX_LEVELS) SetMatrixLevels(voice, index, command.value.levels, command.count); + else if (id == COMMAND_PORT_VOLUME) PortAt(voice, index).volume = command.value.f; + else SetPortMatrix(voice, index, command.value.i); + } + } catch (const Ngs2ReturnCode& result) { + return result.code; } return SCE_NGS2_OK; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp index 6a0c74f5b3..b72cc16451 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp @@ -22,20 +22,28 @@ static void PutTag(std::vector& out, const char* tag) { out.insert(out.end(), tag, tag + 4); } -static std::vector PcmFile(std::uint32_t channels, std::uint32_t sampleRate, std::uint32_t bits, std::uint32_t frames) { +static std::vector PcmFile(std::uint32_t channels, std::uint32_t sampleRate, std::uint32_t bits, std::uint32_t frames, std::uint32_t tag = 1) { const std::uint32_t frameBytes = channels * bits / 8; + const std::uint32_t formatSize = tag == 0xfffe ? 40 : 16; std::vector file; PutTag(file, "RIFF"); - Put32(file, 36 + frames * frameBytes); + Put32(file, 20 + formatSize + frames * frameBytes); PutTag(file, "WAVE"); PutTag(file, "fmt "); - Put32(file, 16); - Put16(file, 1); + Put32(file, formatSize); + Put16(file, tag); Put16(file, channels); Put32(file, sampleRate); Put32(file, sampleRate * frameBytes); Put16(file, frameBytes); Put16(file, bits); + if (tag == 0xfffe) { + Put16(file, 22); + Put16(file, bits); + Put32(file, 0); + const std::uint8_t pcm[16] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}; + file.insert(file.end(), pcm, pcm + sizeof(pcm)); + } PutTag(file, "data"); Put32(file, frames * frameBytes); file.resize(file.size() + frames * frameBytes, 0x11); @@ -60,8 +68,24 @@ static void TestParsePcm() { std::memcpy(partialFrame.data() + 40, &partialSize, sizeof(partialSize)); Require(sceNgs2ParseWaveformData(partialFrame.data(), partialFrame.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); - const auto eightBit = PcmFile(1, 44100, 8, 10); - Require(sceNgs2ParseWaveformData(eightBit.data(), eightBit.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); + struct Depth { std::uint32_t tag, bits, type; }; + for (const auto depth : {Depth{1, 8, 0x11}, Depth{1, 16, 0x12}, Depth{1, 24, 0x14}, Depth{1, 32, 0x16}, Depth{3, 32, 0x18}, Depth{3, 64, 0x1a}, + Depth{0xfffe, 8, 0x11}, Depth{0xfffe, 24, 0x14}}) { + const auto wave = PcmFile(3, 48000, depth.bits, 10, depth.tag); + const std::uint32_t frameBytes = 3 * depth.bits / 8; + const std::uint32_t dataOffset = static_cast(wave.size() - 10 * frameBytes); + Ngs2WaveformInfo parsed{}; + Require(sceNgs2ParseWaveformData(wave.data(), wave.size(), &parsed) == SCE_NGS2_OK); + Require(parsed.format.waveform_type == depth.type && parsed.format.num_channels == 3 && parsed.format.sample_rate == 48000); + Require(parsed.data_offset == dataOffset && parsed.data_size == 10 * frameBytes && parsed.num_samples == 10); + Require(parsed.audio_unit_size == frameBytes && parsed.num_audio_unit_samples == 1 && parsed.audio_frame_size == frameBytes && parsed.num_audio_frame_samples == 1); + Require(parsed.num_blocks == 1 && parsed.block[0].data_offset == dataOffset && parsed.block[0].data_size == 10 * frameBytes && parsed.block[0].num_samples == 10); + } + for (const auto depth : {Depth{1, 12, 0}, Depth{1, 64, 0}, Depth{3, 16, 0}, Depth{3, 24, 0}, Depth{0xfffe, 12, 0}}) { + const auto wave = PcmFile(1, 44100, depth.bits, 8, depth.tag); + Ngs2WaveformInfo parsed{}; + Require(sceNgs2ParseWaveformData(wave.data(), wave.size(), &parsed) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT); + } const auto noChannels = PcmFile(0, 44100, 16, 10); Require(sceNgs2ParseWaveformData(noChannels.data(), noChannels.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Pcm.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Pcm.cpp new file mode 100644 index 0000000000..a22a766f56 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Pcm.cpp @@ -0,0 +1,221 @@ +#include "Ngs2Test.hpp" + +#include +#include +#include +#include + +struct PcmFixture { + std::uint32_t type; + std::uint32_t bytes; + std::vector data; + std::array samples; +}; + +static const PcmFixture Fixtures[] = { + {SCE_NGS2_WAVEFORM_TYPE_PCM_U8, 1, + {0x00, 0x40, 0x60, 0x80, 0xa0, 0xc0, 0xe0, 0xff}, + {-1, -0.5f, -0.25f, 0, 0.25f, 0.5f, 0.75f, 127.0f / 128}}, + {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, + {0x00, 0x80, 0x00, 0xc0, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x20, 0x00, 0x40, 0x00, 0x60, 0xff, 0x7f}, + {-1, -0.5f, -0.25f, 0, 0.25f, 0.5f, 0.75f, 32767.0f / 32768}}, + {SCE_NGS2_WAVEFORM_TYPE_PCM_I24L, 3, + {0x00, 0x00, 0x80, 0x00, 0x00, 0xc0, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x60, 0xff, 0xff, 0x7f}, + {-1, -0.5f, -0.25f, 0, 0.25f, 0.5f, 0.75f, 8388607.0f / 8388608}}, + {SCE_NGS2_WAVEFORM_TYPE_PCM_I32L, 4, + {0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x60, 0xff, 0xff, 0xff, 0x7f}, + {-1, -0.5f, -0.25f, 0, 0.25f, 0.5f, 0.75f, 1}}, + {SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 4, + {0x00, 0x00, 0x80, 0xbf, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x80, 0xbe, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x80, 0x3e, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x40, 0x3f, 0x00, 0x00, 0x80, 0x3f}, + {-1, -0.5f, -0.25f, 0, 0.25f, 0.5f, 0.75f, 1}}, + {SCE_NGS2_WAVEFORM_TYPE_PCM_F64L, 8, + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xbf, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd0, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd0, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x3f, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe8, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f}, + {-1, -0.5f, -0.25f, 0, 0.25f, 0.5f, 0.75f, 1}}, +}; + +class Playback { +public: + Playback(std::uint32_t type, std::uint32_t channels = 1, std::uint32_t rate = 48000) + : firstBuffer(usedBuffers), system(CreateSystem()), channels(channels), voice(Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER))) { + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {type, channels, rate, 0, 0, 0}}); + Patch(voice, Mastering(system, channels)); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + } + + ~Playback() { + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + usedBuffers = firstBuffer; + } + + void Add(const void* data, const Ngs2WaveformBlock* blocks, std::uint32_t count, std::uint32_t flags = 0) { + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, data, flags, count, blocks}); + } + + std::vector Render() { + std::vector output(Grain * channels, 123); + const Ngs2RenderBufferInfo info{output.data(), output.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, channels}; + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); + return output; + } + + Ngs2SamplerVoiceState State() { + Ngs2SamplerVoiceState state{}; + Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); + return state; + } + + const std::size_t firstBuffer; + const std::uintptr_t system; + const std::uint32_t channels; + const std::uintptr_t voice; +}; + +template +static void RequireThrows(TFunction function) { + bool threw = false; + try { function(); } catch (const std::exception&) { threw = true; } + Require(threw); +} + +static void TestFramesAndPositions(const PcmFixture& fixture, std::uint32_t channels) { + Playback playback(fixture.type, channels); + std::vector data(3, 0xa5); + constexpr std::uint32_t skip = 3; + for (std::uint32_t frame = 0; frame < skip + Grain * 2; frame++) { + for (std::uint32_t channel = 0; channel < channels; channel++) { + const auto sample = (frame + channel) % fixture.samples.size(); + const auto first = fixture.data.begin() + sample * fixture.bytes; + data.insert(data.end(), first, first + fixture.bytes); + } + } + const auto frameBytes = channels * fixture.bytes; + const Ngs2WaveformBlock block{2, data.size() - 3, 0, skip, Grain * 2, 0, 0}; + playback.Add(data.data() + 1, &block, 1); + Control(playback.voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{}); + Require(playback.State().waveform_data == data.data() + 3 + skip * frameBytes); + for (std::uint32_t pass = 0; pass < 2; pass++) { + const auto output = playback.Render(); + for (std::uint32_t frame = 0; frame < Grain; frame++) { + for (std::uint32_t channel = 0; channel < channels; channel++) { + const auto actual = output[frame * channels + channel]; + const auto expected = fixture.samples[(skip + pass * Grain + frame + channel) % 8]; + if (actual != expected) { + std::fprintf(stderr, "PCM type %#x frame %u channel %u: got %a, expected %a\n", fixture.type, pass * Grain + frame, channel, actual, expected); + } + Require(actual == expected); + } + } + const auto state = playback.State(); + Require(state.num_decoded_samples == (pass + 1) * Grain); + Require(state.decoded_data_size == (pass + 1) * Grain * frameBytes); + Require(state.waveform_data == data.data() + 3 + (skip + (pass + 1) * Grain) * frameBytes); + } + Require(Flags(playback.voice) == 0); +} + +static std::vector callbacks; + +static void APS5_VABI OnBlock(const Ngs2VoiceCallbackInfo* info) { + callbacks.push_back(*info); +} + +static void TestRepeatAndTransition(const PcmFixture& fixture) { + Playback playback(fixture.type); + callbacks.clear(); + Control(playback.voice, SCE_NGS2_VOICE_PARAM_CALLBACK, + Ngs2VoiceCallbackParam{{}, OnBlock, 7, SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_REPEAT | SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); + const Ngs2WaveformBlock blocks[] = {{0, fixture.bytes * 4, 1, 1, 3, 0, 101}, {fixture.bytes * 4, fixture.bytes * 4, 0, 0, 4, 0, 202}}; + playback.Add(fixture.data.data(), blocks, 2); + const auto first = playback.Render(); + constexpr std::uint32_t indices[] = {1, 2, 3, 1, 2, 3, 4, 5}; + for (std::uint32_t i = 0; i < Grain; i++) Require(first[i] == fixture.samples[indices[i]]); + auto state = playback.State(); + Require(state.num_decoded_samples == 8 && state.decoded_data_size == 8 * fixture.bytes); + Require(state.waveform_data == fixture.data.data() + 6 * fixture.bytes); + const auto last = playback.Render(); + Require(last[0] == fixture.samples[6] && last[1] == fixture.samples[7]); + for (std::uint32_t i = 2; i < Grain; i++) Require(last[i] == 0); + state = playback.State(); + Require(state.num_decoded_samples == 10 && state.decoded_data_size == 10 * fixture.bytes); + Require(state.waveform_data == fixture.data.data() + fixture.data.size()); + Require(callbacks.size() == 3); + Require(callbacks[0].flag == SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_REPEAT && callbacks[0].user_data == 101 && callbacks[0].num_repeated == 1); + Require(callbacks[1].flag == SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END && callbacks[1].user_data == 101); + Require(callbacks[2].flag == SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END && callbacks[2].user_data == 202); + Require(callbacks[0].block_data == fixture.data.data() && callbacks[2].block_data == fixture.data.data() + fixture.bytes * 4); +} + +static void TestResampling(const PcmFixture& fixture) { + Playback playback(fixture.type, 1, 24000); + const Ngs2WaveformBlock blocks[] = {{0, fixture.bytes * 3, 0, 0, 3, 0, 0}, {fixture.bytes * 3, fixture.bytes * 5, 0, 0, 5, 0, 0}}; + playback.Add(fixture.data.data(), blocks, 2); + for (std::uint32_t pass = 0; pass < 2; pass++) { + const auto output = playback.Render(); + for (std::uint32_t i = 0; i < Grain; i++) { + const auto sample = (pass * Grain + i) / 2; + const auto next = sample < 7 ? sample + 1 : sample; + const auto expected = i % 2 == 0 ? fixture.samples[sample] : std::lerp(fixture.samples[sample], fixture.samples[next], 0.5f); + Require(output[i] == expected); + } + Require(playback.State().decoded_data_size == (pass + 1) * 4 * fixture.bytes); + } + Require(playback.State().num_decoded_samples == 8); +} + +static void TestPitch(const PcmFixture& fixture) { + Playback playback(fixture.type); + Control(playback.voice, SCE_NGS2_SAMPLER_VOICE_PARAM_PITCH, Ngs2SamplerVoicePitchParam{{}, 2}); + const Ngs2WaveformBlock block{0, fixture.data.size(), 1, 0, 8, 0, 0}; + playback.Add(fixture.data.data(), &block, 1); + const auto output = playback.Render(); + for (std::uint32_t i = 0; i < Grain; i++) Require(output[i] == fixture.samples[(i * 2) % 8]); + Require(playback.State().num_decoded_samples == 16 && playback.State().decoded_data_size == 16 * fixture.bytes); + Require(Flags(playback.voice) == 0); +} + +static void TestBounds(const PcmFixture& fixture) { + Playback playback(fixture.type, 2); + const Ngs2WaveformBlock shortBlock{0, fixture.data.size() - 1, 1, 0, 4, 0, 0}; + RequireThrows([&] { playback.Add(fixture.data.data(), &shortBlock, 1); }); + const Ngs2WaveformBlock overflow{0, fixture.data.size(), 0, UINT32_MAX, UINT32_MAX, 0, 0}; + RequireThrows([&] { playback.Add(fixture.data.data(), &overflow, 1, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET); }); +} + +static void TestNumericEdges() { + const PcmFixture signed24{SCE_NGS2_WAVEFORM_TYPE_PCM_I24L, 3, + {0x01, 0x00, 0x00, 0xff, 0xff, 0xff, 0x01, 0x00, 0x80, 0xfe, 0xff, 0x7f, + 0x56, 0x34, 0x12, 0xaa, 0xcb, 0xed, 0x80, 0x00, 0x00, 0x80, 0xff, 0xff}, + {0x1p-23f, -0x1p-23f, -8388607.0f / 8388608, 8388606.0f / 8388608, + 1193046.0f / 8388608, -1193046.0f / 8388608, 0x1p-16f, -0x1p-16f}}; + TestFramesAndPositions(signed24, 1); + const PcmFixture signed32{SCE_NGS2_WAVEFORM_TYPE_PCM_I32L, 4, + {0x01, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x00, 0x80, 0xfe, 0xff, 0xff, 0x7f, + 0x01, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x01, 0xfd, 0xff, 0xff, 0xfe}, + {0x1p-31f, -0x1p-31f, -1, 1, 0x1p-7f, 0x1.000002p-7f, 0x1.000004p-7f, -0x1.000004p-7f}}; + TestFramesAndPositions(signed32, 1); + const PcmFixture floating64{SCE_NGS2_WAVEFORM_TYPE_PCM_F64L, 8, + {0x9a, 0x99, 0x99, 0x99, 0x99, 0x99, 0xb9, 0x3f, 0x9a, 0x99, 0x99, 0x99, 0x99, 0x99, 0xb9, 0xbf, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0xe0, 0x3f, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xe0, 0x3f, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xb8, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80}, + {0.1f, -0.1f, 0.5f, 0x1.000004p-1f, 0x1p-126f, -0x1p-126f, 0, 0}}; + TestFramesAndPositions(floating64, 1); +} + +int main() { + for (const auto& fixture : Fixtures) { + for (const auto channels : {1u, 2u, 6u, 8u}) TestFramesAndPositions(fixture, channels); + TestRepeatAndTransition(fixture); + TestResampling(fixture); + TestPitch(fixture); + TestBounds(fixture); + } + TestNumericEdges(); + return 0; +} diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index b15456dae6..f053141be3 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -241,21 +242,58 @@ static void TestPan() { } const Ngs2PanParam front{0.0f, 1.0f, 1.0f, 0.0f}; - float out[2] = {}; + float out[2] = {-1.0f, -1.0f}; Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK); Require(sceNgs2PanGetVolumeMatrix(&work, &front, 1, 2, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); - Require(sceNgs2PanGetVolumeMatrix(&work, &front, 0, 2, out) == SCE_NGS2_OK); - Require(PanThrows(nullptr, &front, 1, 2) && PanThrows(&work, nullptr, 0, 2)); - Require(PanThrows(&work, &front, 1, 4) && PanThrows(&work, &front, 1, 3) && !PanThrows(&work, &front, 1, 8)); + Require(sceNgs2PanGetVolumeMatrix(&work, &front, 0, 2, out) == SCE_NGS2_OK && out[0] == -1.0f && out[1] == -1.0f); + Require(SCE_NGS2_ERROR_INVALID_PAN_UNIT_ANGLE == static_cast(0x804A8450) && SCE_NGS2_ERROR_INVALID_PAN_SPEAKER == static_cast(0x804A8451)); + Require(SCE_NGS2_ERROR_INVALID_PAN_MATRIX_FORMAT == static_cast(0x804A8452) && SCE_NGS2_ERROR_INVALID_PAN_WORK == static_cast(0x804A8453)); + Require(SCE_NGS2_ERROR_INVALID_PAN_PARAM == static_cast(0x804A8454)); + float guard[32]; + const auto panResult = [&](Ngs2PanWork* panWork, const Ngs2PanParam* params, std::uint32_t numParams, std::uint32_t format) { + std::fill(std::begin(guard), std::end(guard), -1.0f); + const int result = sceNgs2PanGetVolumeMatrix(panWork, params, numParams, format, guard); + Require(result == SCE_NGS2_OK || std::all_of(std::begin(guard), std::end(guard), [](float level) { return level == -1.0f; })); + return result; + }; const Ngs2PanParam many[9] = {}; - Require(PanThrows(&work, many, 9, 2) && !PanThrows(&work, many, 8, 2)); + Require(panResult(nullptr, &front, 1, 2) == SCE_NGS2_ERROR_INVALID_PAN_WORK && panResult(nullptr, nullptr, 1, 2) == SCE_NGS2_ERROR_INVALID_PAN_WORK); + Require(sceNgs2PanGetVolumeMatrix(nullptr, &front, 1, 2, nullptr) == SCE_NGS2_ERROR_INVALID_PAN_WORK); + Require(panResult(&work, nullptr, 0, 2) == SCE_NGS2_ERROR_INVALID_PAN_PARAM && panResult(&work, nullptr, 1, 3) == SCE_NGS2_ERROR_INVALID_PAN_PARAM); + Require(sceNgs2PanGetVolumeMatrix(&work, nullptr, 1, 2, nullptr) == SCE_NGS2_ERROR_INVALID_PAN_PARAM); + Require(sceNgs2PanGetVolumeMatrix(&work, &front, 1, 3, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(panResult(&work, &front, 1, 4) == SCE_NGS2_ERROR_INVALID_PAN_MATRIX_FORMAT && panResult(&work, &front, 1, 3) == SCE_NGS2_ERROR_INVALID_PAN_MATRIX_FORMAT); + Require(panResult(&work, many, 9, 3) == SCE_NGS2_ERROR_INVALID_PAN_MATRIX_FORMAT && panResult(&work, &front, 1, 8) == SCE_NGS2_OK); + Require(panResult(&work, many, 9, 2) == SCE_NGS2_ERROR_INVALID_NUM_CHANNELS && panResult(&work, many, 8, 2) == SCE_NGS2_OK); + Ngs2PanWork badWork = work; + badWork.num_speakers = 3; + Require(panResult(&badWork, &front, 1, 2) == SCE_NGS2_ERROR_INVALID_PAN_SPEAKER && panResult(&badWork, &front, 1, 3) == SCE_NGS2_ERROR_INVALID_PAN_MATRIX_FORMAT); + Require(panResult(&badWork, many, 9, 2) == SCE_NGS2_ERROR_INVALID_PAN_SPEAKER); + badWork.unit_angle = 100.0f; + Require(panResult(&badWork, &front, 1, 2) == SCE_NGS2_ERROR_INVALID_PAN_SPEAKER); + badWork.num_speakers = 2; + Require(panResult(&badWork, &front, 1, 2) == SCE_NGS2_ERROR_INVALID_PAN_UNIT_ANGLE && panResult(&badWork, many, 9, 2) == SCE_NGS2_ERROR_INVALID_PAN_UNIT_ANGLE); + Require(sceNgs2PanGetVolumeMatrix(&badWork, &front, 1, 2, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); const Ngs2PanParam nanAngle{NAN, 1.0f, 1.0f, 0.0f}; - Require(PanThrows(&work, &nanAngle, 1, 2)); + Require(panResult(&work, &nanAngle, 1, 2) == SCE_NGS2_OK && guard[0] == 0.0f && guard[1] == 0.0f); const Ngs2PanParam laterNan[2] = {front, nanAngle}; - float untouched[4] = {-1.0f, -1.0f, -1.0f, -1.0f}; - bool rejected = false; - try { sceNgs2PanGetVolumeMatrix(&work, laterNan, 2, 2, untouched); } catch (const std::invalid_argument&) { rejected = true; } - Require(rejected && std::all_of(std::begin(untouched), std::end(untouched), [](float level) { return level == -1.0f; })); + Require(panResult(&work, laterNan, 2, 2) == SCE_NGS2_OK); + Require(std::fabs(guard[0] - 0.707106769f) < 1e-6f && guard[1] == 0.0f && std::fabs(guard[2] - 0.707106769f) < 1e-6f && guard[3] == 0.0f); + Ngs2PanWork sevenWork{}; + Require(sceNgs2PanInit(&sevenWork, nullptr, 360.0f, 7) == SCE_NGS2_OK); + const Ngs2PanParam nanEach[4] = {{NAN, 1.0f, 1.0f, 0.5f}, {30.0f, NAN, 1.0f, 0.5f}, {30.0f, 1.0f, NAN, 0.5f}, {30.0f, 1.0f, 1.0f, NAN}}; + Require(panResult(&sevenWork, nanEach, 4, 8) == SCE_NGS2_OK); + for (std::uint32_t channel = 0; channel < 8; channel++) { + Require(guard[channel * 4] == (channel == 3 ? 0.5f : 0.0f)); + for (std::uint32_t p = 1; p < 4; p++) Require(std::isnan(guard[channel * 4 + p])); + } + Ngs2PanWork radianWork{}; + const float radianPair[] = {-0.52359879f, 0.52359879f}; + Require(sceNgs2PanInit(&radianWork, radianPair, 2.0f * pi, 2) == SCE_NGS2_OK && radianWork.speaker_angles[0] == 0.52359879f); + const Ngs2PanParam belowFirst{0.523598731f, 1.0f, 1.0f, 0.0f}; + const Ngs2PanParam aboveFirst{0.52359885f, 1.0f, 1.0f, 0.0f}; + Require(std::nextafter(0.52359879f, 0.0f) == belowFirst.angle && PanThrows(&radianWork, &belowFirst, 1, 1)); + Require(panResult(&radianWork, &aboveFirst, 1, 1) == SCE_NGS2_OK && std::fabs(guard[0] - 0.707106769f) < 1e-6f); const Ngs2PanParam huge{1e30f, 1.0f, 1.0f, 0.0f}; Require(PanThrows(&work, &huge, 1, 2)); const float hugeSpeaker[] = {1e30f, 30.0f}; @@ -264,6 +302,9 @@ static void TestPan() { Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK); work.speaker_angles[0] = -INFINITY; Require(PanThrows(&work, &front, 1, 2)); + work.speaker_angles[0] = NAN; + const Ngs2PanParam right{30.0f, 1.0f, 1.0f, 0.0f}; + Require(panResult(&work, &right, 1, 2) == SCE_NGS2_OK && guard[0] == 0.0f && guard[1] == 0.0f); Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK); Ngs2PanWork marked; std::memset(&marked, 0x5a, sizeof(marked)); @@ -278,8 +319,14 @@ static void TestPan() { Require(sceNgs2PanInit(&marked, nullptr, 360.0f, count) == static_cast(0x804A8451)); Require(unchanged()); } - Require(PanInitThrows(&marked, nullptr, 360.0f, 0)); - Require(unchanged()); + const float sevenDegrees[] = {0.0f, 40.0f, 110.0f, 150.0f, 210.0f, 250.0f, 320.0f, 360.0f}; + for (const float unitAngle : {360.0f, 2.0f * pi, 180.0f, NAN}) { + Ngs2PanWork seven; + std::memset(&seven, 0x5a, sizeof(seven)); + Require(sceNgs2PanInit(&seven, nullptr, unitAngle, 0) == SCE_NGS2_OK && seven.num_speakers == 7); + Require(seven.unit_angle == (unitAngle == 2.0f * pi ? 2.0f * pi : 360.0f)); + for (std::uint32_t i = 0; i < 8; i++) Require(seven.speaker_angles[i] == sevenDegrees[i]); + } for (std::uint32_t count : {0u, 1u, 3u, 6u, 8u, 255u}) { Require(sceNgs2PanInit(&marked, pair, 360.0f, count) == static_cast(0x804A8451)); Require(sceNgs2PanInit(&marked, pair, 2.0f * pi, count) == static_cast(0x804A8451)); @@ -291,13 +338,31 @@ static void TestPan() { Require(unchanged()); } Require(sceNgs2PanInit(&work, pair, 2.0f * pi, 2) == SCE_NGS2_OK); - const float invalid[] = {0.0f, NAN}; - Require(PanInitThrows(&work, invalid, 360.0f, 2)); + const float nanSpeaker[] = {0.0f, NAN}; + Require(sceNgs2PanInit(&work, nanSpeaker, 360.0f, 2) == SCE_NGS2_OK && work.num_speakers == 2 && work.unit_angle == 360.0f); + Require(std::isnan(work.speaker_angles[0]) && work.speaker_angles[1] == 0.0f && std::isnan(work.speaker_angles[2])); const float onUnit[] = {720.0f, 30.0f}; const float belowZero[] = {-360.0f, 30.0f}; Require(PanInitThrows(&work, onUnit, 360.0f, 2) && !PanInitThrows(&work, belowZero, 360.0f, 2)); work.unit_angle = 100.0f; - Require(PanThrows(&work, &front, 1, 2)); + Require(panResult(&work, &front, 1, 2) == SCE_NGS2_ERROR_INVALID_PAN_UNIT_ANGLE); +} + +static void TestPanExplicitAnglesAndNanDistance() { + Ngs2PanWork work; + std::memset(&work, 0x5a, sizeof(work)); + Ngs2PanWork filled; + std::memcpy(&filled, &work, sizeof(work)); + const float pair[] = {-30.0f, 30.0f}; + Require(sceNgs2PanInit(&work, pair, 360.0f, 2) == SCE_NGS2_OK && work.num_speakers == 2 && work.unit_angle == 360.0f); + Require(work.speaker_angles[0] == 30.0f && work.speaker_angles[1] == 330.0f && work.speaker_angles[2] == 390.0f); + Require(std::memcmp(&work.speaker_angles[3], &filled.speaker_angles[3], 5 * sizeof(float)) == 0); + Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK); + for (std::uint32_t i = 3; i < 8; i++) Require(work.speaker_angles[i] == 0.0f); + const Ngs2PanParam params[2] = {{0.0f, 1.0f, 1.0f, 0.0f}, {30.0f, NAN, 1.0f, 0.0f}}; + float out[4] = {}; + Require(sceNgs2PanGetVolumeMatrix(&work, params, 2, 2, out) == SCE_NGS2_OK); + Require(std::fabs(out[0] - 0.707106769f) < 1e-6f && std::isnan(out[1]) && std::fabs(out[2] - 0.707106769f) < 1e-6f && std::isnan(out[3])); } static void TestPitchAndRepeat() { @@ -944,9 +1009,6 @@ static void TestMasteringGain() { Control(toQuad, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, 0, 0}); Patch(toQuad, quad); Event(toQuad, SCE_NGS2_VOICE_EVENT_PLAY); - bool rejected = false; - try { Control(stereo, SCE_NGS2_MASTERING_VOICE_PARAM_GAIN, Ngs2MasteringVoiceGainParam{{}, NAN, 1.0f}); } catch (const std::exception&) { rejected = true; } - Require(rejected); std::vector outStereo(Grain * 2, -1.0f); std::vector outSurround(Grain * 6, -1.0f); @@ -980,6 +1042,80 @@ static void TestMasteringGain() { Require(sceNgs2SystemDestroy(loudSystem, nullptr) == SCE_NGS2_OK); } +static void TestMasteringGainClampedLikePs5() { + std::deque> waveforms; + const auto chain = [&waveforms](std::uint32_t channels, std::uint32_t grains, uintptr_t& master) { + const auto system = CreateSystem(); + master = Mastering(system, channels); + const auto& pcm = waveforms.emplace_back(Grain * grains, 16384); + const auto source = Sampler(system, pcm, 0); + const std::vector levels(channels, 1.0f); + Control(source, SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS, Ngs2VoiceMatrixLevelsParam{{}, 0, channels, levels.data()}); + Control(source, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, 0, 0}); + Patch(source, master); + Event(source, SCE_NGS2_VOICE_EVENT_PLAY); + return system; + }; + const auto render = [](uintptr_t system, std::uint32_t channels) { + std::vector out(Grain * channels, -1.0f); + const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, channels}; + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); + return out; + }; + const auto expect = [](const std::vector& out, std::uint32_t channels, float fbwOut, float lfeOut) { + for (std::uint32_t i = 0; i < Grain; i++) { + for (std::uint32_t channel = 0; channel < channels; channel++) { + const float level = channel == 3 && channels >= 6 ? lfeOut : fbwOut; + const float sample = out[i * channels + channel]; + Require(std::isnan(level) ? std::isnan(sample) : sample == 0.5f * level); + } + } + }; + const auto gain = [&](std::uint32_t channels, float fbw, float lfe, float fbwOut, float lfeOut) { + uintptr_t master = 0; + const auto system = chain(channels, 1, master); + Control(master, SCE_NGS2_MASTERING_VOICE_PARAM_GAIN, Ngs2MasteringVoiceGainParam{{}, fbw, lfe}); + expect(render(system, channels), channels, fbwOut, lfeOut); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + }; + const float inf = INFINITY; + for (const std::uint32_t channels : {6u, 8u}) { + gain(channels, 32.0f, 1.0f, 32.0f, 1.0f); + gain(channels, 33.0f, 1.0f, 32.0f, 1.0f); + gain(channels, 64.0f, 1.0f, 32.0f, 1.0f); + gain(channels, 1e30f, 1.0f, 32.0f, 1.0f); + gain(channels, inf, 1.0f, 32.0f, 1.0f); + gain(channels, -32.0f, 1.0f, -32.0f, 1.0f); + gain(channels, -33.0f, 1.0f, -32.0f, 1.0f); + gain(channels, -inf, 1.0f, -32.0f, 1.0f); + gain(channels, NAN, 1.0f, NAN, 1.0f); + gain(channels, 1.0f, 32.0f, 1.0f, 32.0f); + gain(channels, 1.0f, 33.0f, 1.0f, 32.0f); + gain(channels, 1.0f, 1e30f, 1.0f, 32.0f); + gain(channels, 1.0f, inf, 1.0f, 32.0f); + gain(channels, 1.0f, -33.0f, 1.0f, -32.0f); + gain(channels, 1.0f, -inf, 1.0f, -32.0f); + gain(channels, 1.0f, NAN, 1.0f, NAN); + } + gain(1, 0.5f, 2.0f, 0.5f, 0.5f); + gain(1, inf, NAN, 32.0f, 32.0f); + gain(2, -inf, 1e30f, -32.0f, -32.0f); + gain(2, 2.0f, 0.5f, 2.0f, 2.0f); + + uintptr_t master = 0; + const auto system = chain(6, 4, master); + expect(render(system, 6), 6, 1.0f, 1.0f); + Control(master, SCE_NGS2_MASTERING_VOICE_PARAM_GAIN, Ngs2MasteringVoiceGainParam{{}, 0.5f, 0.25f}); + expect(render(system, 6), 6, 0.5f, 0.25f); + Event(master, SCE_NGS2_VOICE_EVENT_STOP_IMM); + Event(master, SCE_NGS2_VOICE_EVENT_PLAY); + expect(render(system, 6), 6, 0.5f, 0.25f); + Control(master, SCE_NGS2_MASTERING_VOICE_PARAM_SETUP, Ngs2MasteringVoiceSetupParam{{}, 6, 0}); + Event(master, SCE_NGS2_VOICE_EVENT_PLAY); + expect(render(system, 6), 6, 1.0f, 1.0f); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + static uintptr_t RackVoice(uintptr_t rack, std::uint32_t index) { uintptr_t voice = 0; Require(sceNgs2RackGetVoiceHandle(rack, index, &voice) == SCE_NGS2_OK && voice != 0); @@ -1658,12 +1794,16 @@ int main() { TestPcmBlockEnd(); TestPatchToRackHandle(); TestPan(); + TestPanExplicitAnglesAndNanDistance(); TestPitchAndRepeat(); TestSubmixerMatrix(); TestReverb(); TestReverbSetup(); TestReverbConversion(); TestSampleRate(); + + TestMasteringGainClampedLikePs5(); + TestSystemOptionFlags(); TestUserData(); TestPcmCallbackReset(); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceEvents.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceEvents.cpp new file mode 100644 index 0000000000..da4b91132b --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceEvents.cpp @@ -0,0 +1,103 @@ +#include "Ngs2Test.hpp" + +#include +#include + +static const Ngs2SamplerVoiceSetupParam SamplerSetup{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}; + +static Ngs2VoiceEventParam PlayParam() { + return {{sizeof(Ngs2VoiceEventParam), 0, SCE_NGS2_VOICE_PARAM_EVENT}, SCE_NGS2_VOICE_EVENT_PLAY}; +} + +static uintptr_t Sampler(uintptr_t system) { + const auto master = Mastering(system, 1); + const auto voice = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); + Patch(voice, master); + return voice; +} + +static void TestPlayWithoutSetup() { + const auto system = CreateSystem(); + const auto voice = Sampler(system); + const auto play = PlayParam(); + const Ngs2VoiceCommand playCommand{2, 0, 4, 0, {.u = SCE_NGS2_VOICE_EVENT_PLAY}}; + Event(voice, SCE_NGS2_VOICE_EVENT_STOP_IMM); + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, nullptr, 0, 0, nullptr}); + Require(sceNgs2VoiceControl(voice, &play.header) == SCE_NGS2_ERROR_INVALID_VOICE_ACCESS); + Require(sceNgs2VoiceRunCommands(voice, &playCommand, 1) == SCE_NGS2_ERROR_INVALID_VOICE_ACCESS); + + std::vector out(Grain, -1.0f); + const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1}; + for (int grain = 0; grain < 3; grain++) { + Require(Flags(voice) == 0); + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); + for (float sample : out) Require(sample == 0.0f); + } + Require(Flags(voice) == 0); + + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, SamplerSetup); + Require(sceNgs2VoiceRunCommands(voice, &playCommand, 1) == SCE_NGS2_OK); + Require(Flags(voice) == SCE_NGS2_VOICE_STATE_FLAG_INUSE); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestEmptyFormatKeepsTheSetup() { + const auto system = CreateSystem(); + const auto voice = Sampler(system); + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, SamplerSetup); + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{}); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + Require(Flags(voice) == SCE_NGS2_VOICE_STATE_FLAG_INUSE); + + const auto bare = Sampler(system); + Control(bare, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{}); + const auto play = PlayParam(); + Require(sceNgs2VoiceControl(bare, &play.header) == SCE_NGS2_ERROR_INVALID_VOICE_ACCESS); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestPlayAfterSetupInOneList() { + const auto system = CreateSystem(); + const auto voice = Sampler(system); + struct List { + Ngs2SamplerVoiceSetupParam setup; + Ngs2VoiceEventParam play; + }; + List list{SamplerSetup, PlayParam()}; + list.setup.header = {sizeof(list.setup), static_cast(sizeof(list.setup)), SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP}; + Require(sceNgs2VoiceControl(voice, &list.setup.header) == SCE_NGS2_OK); + Require(Flags(voice) == SCE_NGS2_VOICE_STATE_FLAG_INUSE); + + const auto other = Sampler(system); + struct Reversed { + Ngs2VoiceEventParam play; + Ngs2SamplerVoiceSetupParam setup; + }; + Reversed reversed{PlayParam(), SamplerSetup}; + reversed.play.header = {sizeof(reversed.play), static_cast(sizeof(reversed.play)), SCE_NGS2_VOICE_PARAM_EVENT}; + reversed.setup.header = {sizeof(reversed.setup), 0, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP}; + Require(sceNgs2VoiceControl(other, &reversed.play.header) == SCE_NGS2_ERROR_INVALID_VOICE_ACCESS); + Require(Flags(other) == 0); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestNeverSetUpVoicesOfOtherRacks() { + const auto system = CreateSystem(); + const auto play = PlayParam(); + for (const auto rackId : {SCE_NGS2_RACK_ID_SUBMIXER, SCE_NGS2_RACK_ID_REVERB, SCE_NGS2_RACK_ID_MASTERING}) { + const auto voice = Voice(CreateRack(system, rackId)); + Require(sceNgs2VoiceControl(voice, &play.header) == SCE_NGS2_ERROR_INVALID_VOICE_ACCESS); + Require(Flags(voice) == 0); + } + const auto master = Mastering(system, 1); + Require(Flags(master) == SCE_NGS2_VOICE_STATE_FLAG_INUSE); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +int main() { + TestPlayWithoutSetup(); + TestEmptyFormatKeepsTheSetup(); + TestPlayAfterSetupInOneList(); + TestNeverSetUpVoicesOfOtherRacks(); + return 0; +} diff --git a/core/libs/prx/libSceNgs2/CMakeLists.txt b/core/libs/prx/libSceNgs2/CMakeLists.txt index 41b6cfe96a..116315109b 100644 --- a/core/libs/prx/libSceNgs2/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2/CMakeLists.txt @@ -12,4 +12,5 @@ if(BUILD_TESTING) set_tests_properties(ngs2_core_render PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_sce_ngs2_test(libSceNgs2 ngs2_core_pcm Ngs2Pcm.cpp) endif() diff --git a/core/libs/prx/libSceNotification/Export.cpp b/core/libs/prx/libSceNotification/Export.cpp index 3431c8dbc3..8d6772d02d 100644 --- a/core/libs/prx/libSceNotification/Export.cpp +++ b/core/libs/prx/libSceNotification/Export.cpp @@ -5,10 +5,10 @@ extern "C" { -int APS5_VABI sceNotificationSend(int type, std::uint8_t flag, std::uint8_t* message) { - (void)type; - (void)flag; - (void)message; +int APS5_VABI sceNotificationSend(int userId, bool logged, const char* payload) { + (void)userId; + (void)logged; + (void)payload; NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceNotification/include/Api.hpp b/core/libs/prx/libSceNotification/include/Api.hpp index 24cb956b82..7174c3528a 100644 --- a/core/libs/prx/libSceNotification/include/Api.hpp +++ b/core/libs/prx/libSceNotification/include/Api.hpp @@ -4,7 +4,7 @@ extern "C" { -int APS5_VABI sceNotificationSend(int type, std::uint8_t flag, std::uint8_t* message); +int APS5_VABI sceNotificationSend(int userId, bool logged, const char* payload); int APS5_VABI sceNotificationSendById(int type, std::uint8_t flag, std::uint8_t* id, std::uint8_t* jsonData); int APS5_VABI sceNotificationShowPsButtonPersistentBanner(std::uint8_t* jsonParam); void APS5_VABI sceNotificationHidePsButtonPersistentBanner(); diff --git a/core/libs/prx/libSceNpCommon/PendingExports.cpp b/core/libs/prx/libSceNpCommon/PendingExports.cpp index 7c3808ebf3..b6479f111b 100644 --- a/core/libs/prx/libSceNpCommon/PendingExports.cpp +++ b/core/libs/prx/libSceNpCommon/PendingExports.cpp @@ -6,11 +6,6 @@ extern "C" { -int APS5_VABI sceNpCloseEventFlag(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpCloseSema(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 8c8edda9b4..510328687f 100644 --- a/core/libs/prx/libSceNpCommon/Runtime.cpp +++ b/core/libs/prx/libSceNpCommon/Runtime.cpp @@ -326,6 +326,10 @@ int APS5_VABI sceNpOpenEventFlag(KernelEventFlag* flag, const char* name) { return NpResult(sceKernelOpenEventFlag(flag, name)); } +int APS5_VABI sceNpCloseEventFlag(KernelEventFlag flag) { + return NpResult(sceKernelCloseEventFlag(flag)); +} + int APS5_VABI sceNpSetEventFlag(KernelEventFlag flag, std::uint64_t pattern) { return NpResult(sceKernelSetEventFlag(flag, pattern)); } diff --git a/core/libs/prx/libSceNpCommon/include/Api.hpp b/core/libs/prx/libSceNpCommon/include/Api.hpp index 6e1bf26bc7..d4c373e182 100644 --- a/core/libs/prx/libSceNpCommon/include/Api.hpp +++ b/core/libs/prx/libSceNpCommon/include/Api.hpp @@ -87,7 +87,6 @@ int APS5_VABI sceNpCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, std::ui int APS5_VABI sceNpJoinThread(Pthread thread, void** retval); int APS5_VABI sceNpGetSystemClockUsec(std::int64_t* usec); void APS5_VABI sceNpGetSdkVersion(char* version); -int APS5_VABI sceNpCloseEventFlag(void); int APS5_VABI sceNpCloseSema(void); int APS5_VABI sceNpGetNavSdkVersion(void); int APS5_VABI sceNpIntGetPlatformType(void); @@ -137,6 +136,7 @@ int APS5_VABI sceNpLwCondWait(PthreadCond* cond, PthreadMutex* mutex); int APS5_VABI sceNpCreateEventFlag(KernelEventFlag* flag, const char* name, std::uint32_t attr, std::uint64_t pattern); int APS5_VABI sceNpDeleteEventFlag(KernelEventFlag flag); int APS5_VABI sceNpOpenEventFlag(KernelEventFlag* flag, const char* name); +int APS5_VABI sceNpCloseEventFlag(KernelEventFlag flag); int APS5_VABI sceNpSetEventFlag(KernelEventFlag flag, std::uint64_t pattern); int APS5_VABI sceNpClearEventFlag(KernelEventFlag flag, std::uint64_t pattern); int APS5_VABI sceNpPollEventFlag(KernelEventFlag flag, std::uint64_t pattern, std::uint32_t mode, std::uint64_t* result); diff --git a/core/libs/prx/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp index 6ab09331ae..bf1e1383dc 100644 --- a/core/libs/prx/libSceNpCppWebApi/Export.cpp +++ b/core/libs/prx/libSceNpCppWebApi/Export.cpp @@ -203,16 +203,55 @@ int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi12downloadDataEi return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; } -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData10initializeEPNS1_6Common10LibContextEPKci() { - return 0; +static constexpr int ParameterNotInitialized = static_cast(0x80553502); +static constexpr int ParameterInvalidArgument = static_cast(0x80553503); +static constexpr int ParameterAlreadyInitialized = static_cast(0x8055350f); +static constexpr int ParameterTerminatedUninitialized = static_cast(0x80553510); + +struct ParameterObject { + void* base; + const void* implementation; +}; + +static constexpr unsigned char ParameterImplementation = 0; + +static void ConstructParameter(void* self) { + *static_cast(self) = {}; } -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData9terminateEv() { - return 0; +static int InitializeParameter(void* self, const std::uint64_t* context, const bool argumentsPresent) { + if (context == nullptr) return ParameterInvalidArgument; + if (*context == 0) return ParameterNotInitialized; + auto* parameter = static_cast(self); + if (parameter->implementation != nullptr) return ParameterAlreadyInitialized; + if (!argumentsPresent) return ParameterInvalidArgument; + parameter->implementation = &ParameterImplementation; + return 0; +} + +static int SetParameterText(const void* self, const char* text) { + if (static_cast(self)->implementation == nullptr) return ParameterNotInitialized; + return text == nullptr ? ParameterInvalidArgument : 0; +} + +static int TerminateParameter(void* self) { + auto* parameter = static_cast(self); + if (parameter->implementation == nullptr) return ParameterTerminatedUninitialized; + parameter->implementation = nullptr; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData10initializeEPNS1_6Common10LibContextEPKci(void* self, const std::uint64_t* context, const char* text, int value) { + (void)value; + return InitializeParameter(self, context, text != nullptr); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData9terminateEv(void* self) { + return TerminateParameter(self); } int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadDataC1Ev(void* self) { - (void)self; + ConstructParameter(self); return 0; } @@ -1721,28 +1760,27 @@ void* APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V59Container11getChildrenEv( return nullptr; } -int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainerC1Ev(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainerC1Ev(void* self) { + ConstructParameter(self); return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer10initializeEPNS1_6Common10LibContextE() { - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer10initializeEPNS1_6Common10LibContextE(void* self, const std::uint64_t* context) { + return InitializeParameter(self, context, true); } bool APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer13isInitializedEv(const void* self) { + return static_cast(self)->implementation != nullptr; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer15setserviceLabelEi(void* self, int serviceLabel) { (void)self; - NotImplemented_nid_no_patch(__func__); - return false; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer15setserviceLabelEi(void) { - NotImplemented_nid_no_patch(__func__); + (void)serviceLabel; return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer9terminateEv() { - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer9terminateEv(void* self) { + return TerminateParameter(self); } int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainerD1Ev(void* self) { @@ -1778,33 +1816,29 @@ bool APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product9skusIsSetEv(const return false; } -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData11setobjectIdEPKc(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData11setobjectIdEPKc(void* self, const char* objectId) { + return SetParameterText(self, objectId); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUserC1Ev(void* self) { + ConstructParameter(self); return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUserC1Ev(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser10initializeEPNS1_6Common10LibContextEPKcSA_(void* self, const std::uint64_t* context, const char* first, const char* second) { + return InitializeParameter(self, context, first != nullptr && second != nullptr); } -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser10initializeEPNS1_6Common10LibContextEPKcSA_(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser9setfieldsEPKc(void* self, const char* fields) { + return SetParameterText(self, fields); } -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser9setfieldsEPKc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser9terminateEv(void* self) { + return TerminateParameter(self); } -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser9terminateEv(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUserD1Ev(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUserD1Ev(void* self) { + (void)self; return 0; } @@ -3014,8 +3048,8 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product16releaseDateIsSetE return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer15setcontainerIdsEPKc() { - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer15setcontainerIdsEPKc(void* self, const char* containerIds) { + return SetParameterText(self, containerIds); } int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer8setlimitEi() { diff --git a/core/libs/prx/libSceNpCppWebApi/include/Api.hpp b/core/libs/prx/libSceNpCppWebApi/include/Api.hpp index cdb32647a1..6e537ea7dd 100644 --- a/core/libs/prx/libSceNpCppWebApi/include/Api.hpp +++ b/core/libs/prx/libSceNpCppWebApi/include/Api.hpp @@ -61,8 +61,8 @@ int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi26ParameterToRecord int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi26ParameterToRecordLargeDataC1Ev(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi26ParameterToRecordLargeDataD1Ev(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi12downloadDataEiRKNS4_23ParameterToDownloadDataERNS1_6Common21DownStreamTransactionINS8_12IntrusivePtrINS4_27DownloadDataResponseHeadersEEEEE(); -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData10initializeEPNS1_6Common10LibContextEPKci(); -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData10initializeEPNS1_6Common10LibContextEPKci(void* self, const std::uint64_t* context, const char* text, int value); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData9terminateEv(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadDataC1Ev(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadDataD1Ev(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi6Common10InitParamsC1Ev(void* self); @@ -351,23 +351,23 @@ std::size_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1 void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEE5beginEv(void* self); void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEE3endEv(void* self); void* APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V59Container11getChildrenEv(void* self); -int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainerC1Ev(void); -int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer10initializeEPNS1_6Common10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainerC1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer10initializeEPNS1_6Common10LibContextE(void* self, const std::uint64_t* context); bool APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer13isInitializedEv(const void* self); -int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer15setserviceLabelEi(void); -int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer15setserviceLabelEi(void* self, int serviceLabel); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer9terminateEv(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainerD1Ev(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi12getContainerEiRKNS4_23ParameterToGetContainerERNS1_6Common11TransactionINS8_12IntrusivePtrINS8_6VectorINSA_INS3_9ContainerEEEEEEENSA_INS8_18ResponseHeaderBaseEEEEE(); const char* APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product8getLabelEv(const void* self); void* APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V57Product7getSkusEv(void* self); int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product7getTypeEv(const void* self); bool APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product9skusIsSetEv(const void* self); -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData11setobjectIdEPKc(void); -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUserC1Ev(void); -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser10initializeEPNS1_6Common10LibContextEPKcSA_(void); -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser9setfieldsEPKc(void); -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser9terminateEv(void); -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUserD1Ev(void); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData11setobjectIdEPKc(void* self, const char* objectId); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUserC1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser10initializeEPNS1_6Common10LibContextEPKcSA_(void* self, const std::uint64_t* context, const char* first, const char* second); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser9setfieldsEPKc(void* self, const char* fields); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser9terminateEv(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUserD1Ev(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi26getMultiDataStatusesByUserEiRKNS4_37ParameterToGetMultiDataStatusesByUserERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_32GetMultiDataStatusesResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE(void); std::size_t APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V110DataStatus11getDataSizeEv(const void* self); const char* APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V110DataStatus11getObjectIdEv(const void* self); @@ -636,7 +636,7 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V53Sku25displayOriginalPriceI int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V53Sku8getLabelEv(); int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer9setoffsetEi(); int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product16releaseDateIsSetEv(); -int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer15setcontainerIdsEPKc(); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer15setcontainerIdsEPKc(void* self, const char* containerIds); int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer8setlimitEi(); int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V53Sku17displayPriceIsSetEv(); int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product5getIdEv(); diff --git a/core/libs/prx/libSceNpGameIntent/Export.cpp b/core/libs/prx/libSceNpGameIntent/Export.cpp index 751b4b1c5b..dfea590bc3 100644 --- a/core/libs/prx/libSceNpGameIntent/Export.cpp +++ b/core/libs/prx/libSceNpGameIntent/Export.cpp @@ -1,4 +1,5 @@ #include "prx/libSceNpGameIntent/include/Api.hpp" +#include #include #include #include @@ -8,43 +9,57 @@ namespace { +constexpr int NpGameIntentErrorNotInitialized = -2141898750; constexpr int NpGameIntentErrorInvalidArgument = -2141898748; constexpr int NpGameIntentErrorIntentNotFound = -2141898746; constexpr int NpGameIntentErrorValueNotFound = -2141898745; constexpr int NpGameIntentUserIdInvalid = -1; +std::atomic_bool& NpGameIntentInitialized() { + static std::atomic_bool initialized{false}; + return initialized; +} + } extern "C" { int APS5_VABI sceNpGameIntentGetPropertyValueString(const NpGameIntentData* intentData, const char* key, char* valueBuf, size_t bufSize) { - if (intentData == nullptr || key == nullptr || valueBuf == nullptr || bufSize == 0) { - APS5_INVALID_ARG_EX; - } + if (!NpGameIntentInitialized()) { + return NpGameIntentErrorNotInitialized; + } + if (intentData == nullptr || key == nullptr || key[0] == '\0' || valueBuf == nullptr || bufSize == 0) { + return NpGameIntentErrorInvalidArgument; + } - valueBuf[0] = '\0'; - return NpGameIntentErrorValueNotFound; + valueBuf[0] = '\0'; + return NpGameIntentErrorValueNotFound; } int APS5_VABI sceNpGameIntentInitialize(const void* initParam) { - (void)initParam; - return 0; + (void)initParam; + NpGameIntentInitialized() = true; + return 0; } int APS5_VABI sceNpGameIntentReceiveIntent(NpGameIntentInfo* intentInfo) { - if (intentInfo == nullptr) { - APS5_INVALID_ARG_EX; - } + if (!NpGameIntentInitialized()) { + return NpGameIntentErrorNotInitialized; + } + if (intentInfo == nullptr || intentInfo->size != 0x4140) { + return NpGameIntentErrorInvalidArgument; + } - intentInfo->user_id = NpGameIntentUserIdInvalid; - std::memset(intentInfo->intent_type, 0, sizeof(intentInfo->intent_type)); - std::memset(&intentInfo->intent_data, 0, sizeof(intentInfo->intent_data)); + intentInfo->user_id = NpGameIntentUserIdInvalid; + std::memset(intentInfo->intent_type, 0, sizeof(intentInfo->intent_type)); + std::memset(&intentInfo->intent_data, 0, sizeof(intentInfo->intent_data)); - return NpGameIntentErrorIntentNotFound; + return NpGameIntentErrorIntentNotFound; } int APS5_VABI sceNpGameIntentTerminate(void) { - return 0; + NpGameIntentInitialized() = false; + return 0; } int APS5_VABI sceNpGameIntentInitParamInit(NpGameIntentInitParam* param) { diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index 55f1c4c5c8..ffb8cc5c1e 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -17,7 +17,13 @@ static constexpr int SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED = static_cast static constexpr int SCE_NP_ERROR_CALLBACK_NOT_REGISTERED = static_cast(0x80550009); static constexpr int SCE_NP_ERROR_INVALID_SIZE = static_cast(0x80550011); static constexpr int SCE_NP_ERROR_REQUEST_NOT_FOUND = static_cast(0x80550014); +static constexpr int SCE_NP_ERROR_INVALID_ID = static_cast(0x80550015); static constexpr int SCE_NP_ERROR_CALLBACK_MAX = static_cast(0x8055001D); +static constexpr int NP_ID_MAPPER_REFUSED_ARGUMENT = static_cast(0x80553083); +static constexpr int NP_ID_MAPPER_REFUSED_REQUEST_ID = static_cast(0x80553085); +static constexpr int NP_MANAGER_INT_REQUEST_DUPLICATE = static_cast(0x80559D03); +static constexpr int NP_MANAGER_INT_REQUEST_TABLE_FULL = static_cast(0x80559D05); +static constexpr int NP_MANAGER_INT_REQUEST_NOT_IN_TABLE = static_cast(0x80559D0A); static constexpr uint32_t NP_STATE_SIGNED_OUT = 1; static constexpr int NP_POLL_ASYNC_FINISHED = 0; static constexpr uint32_t NP_REACHABILITY_STATE_UNAVAILABLE = 0; @@ -39,6 +45,14 @@ int AddRequest() { return reqId; } +std::mutex idMapperMutex; +std::uint32_t nextIdMapperRequest = 1; +std::unordered_set idMapperRequests; +constexpr std::size_t NP_MANAGER_INT_REQUEST_SLOTS = 64; +std::mutex managerIntMutex; +std::uint32_t nextManagerIntRequest = 1; +std::unordered_set managerIntRequests; + int SignedOutRequest(int reqId) { if (reqId == 0) return SCE_NP_ERROR_INVALID_ARGUMENT; std::lock_guard lock(requestMutex); @@ -284,9 +298,10 @@ int APS5_VABI sceNpGetParentalControlInfoA(int reqId, int userId, int8_t* age, v if (!age || !info || userId == USER_SERVICE_USER_ID_INVALID) return SCE_NP_ERROR_INVALID_ARGUMENT; return SignedOutRequest(reqId); } -int APS5_VABI sceNpIdMapperAbortRequest(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpIdMapperAbortRequest(int reqId) { + if (reqId <= 0) return NP_ID_MAPPER_REFUSED_REQUEST_ID; + std::lock_guard lock(idMapperMutex); + return idMapperRequests.count(reqId) ? 0 : SCE_NP_ERROR_REQUEST_NOT_FOUND; } int APS5_VABI sceNpIdMapperAccountIdToNpId(void) { NotImplemented_nid_no_patch(__func__); @@ -297,20 +312,29 @@ int APS5_VABI sceNpIdMapperAccountIdToOnlineId(void) { return 0; } int APS5_VABI sceNpIdMapperCreateRequest(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(idMapperMutex); + const int reqId = static_cast(nextIdMapperRequest++); + idMapperRequests.insert(reqId); + return reqId; } -int APS5_VABI sceNpIdMapperDeleteRequest(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpIdMapperDeleteRequest(int reqId) { + if (reqId <= 0) return NP_ID_MAPPER_REFUSED_REQUEST_ID; + std::lock_guard lock(idMapperMutex); + return idMapperRequests.erase(reqId) ? 0 : SCE_NP_ERROR_REQUEST_NOT_FOUND; } -int APS5_VABI sceNpIdMapperNpIdToAccountId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +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) { + (void)flag; + (void)extra; + if (reqId <= 0) return NP_ID_MAPPER_REFUSED_REQUEST_ID; + 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 sceNpIdMapperOnlineIdToAccountId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +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) { + (void)flag; + (void)extra; + if (reqId <= 0) return NP_ID_MAPPER_REFUSED_REQUEST_ID; + 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__); @@ -361,9 +385,9 @@ int APS5_VABI sceNpIsPlusMember(int, bool* isMember) { *isMember = false; return 0; } -int APS5_VABI sceNpManagerIntAbortRequest(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpManagerIntAbortRequest(int reqId) { + std::lock_guard lock(managerIntMutex); + return managerIntRequests.count(reqId) ? 0 : SCE_NP_ERROR_REQUEST_NOT_FOUND; } int APS5_VABI sceNpManagerIntAddActiveSigninStateCallback(void) { NotImplemented_nid_no_patch(__func__); @@ -438,16 +462,20 @@ int APS5_VABI sceNpManagerIntCreateLoginRequest(void) { return 0; } int APS5_VABI sceNpManagerIntCreateRequest(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(managerIntMutex); + if (managerIntRequests.size() >= NP_MANAGER_INT_REQUEST_SLOTS) return NP_MANAGER_INT_REQUEST_TABLE_FULL; + const int reqId = static_cast(nextManagerIntRequest++); + if (reqId <= 0) return reqId; + if (!managerIntRequests.insert(reqId).second) return NP_MANAGER_INT_REQUEST_DUPLICATE; + return reqId; } int APS5_VABI sceNpManagerIntDeleteLoginContext(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpManagerIntDeleteRequest(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpManagerIntDeleteRequest(int reqId) { + std::lock_guard lock(managerIntMutex); + return managerIntRequests.erase(reqId) ? 0 : NP_MANAGER_INT_REQUEST_NOT_IN_TABLE; } int APS5_VABI sceNpManagerIntGetAccessTokenViaImplicitFlow(void) { NotImplemented_nid_no_patch(__func__); @@ -955,7 +983,7 @@ int APS5_VABI sceNpRegisterStateCallbackA(NpStateCallbackA callback, void* userd } int APS5_VABI sceNpUnregisterStateCallbackA(int callbackId) { - if (callbackId < 1 || callbackId > NP_STATE_CALLBACK_MAX) return SCE_NP_ERROR_INVALID_ARGUMENT; + if (static_cast(callbackId) - 1u >= static_cast(NP_STATE_CALLBACK_MAX)) return SCE_NP_ERROR_INVALID_ID; std::lock_guard lock(stateMutex); auto& entry = stateCallbacksA[callbackId - 1]; if (!entry.callback) return SCE_NP_ERROR_CALLBACK_NOT_REGISTERED; diff --git a/core/libs/prx/libSceNpManager/include/Api.hpp b/core/libs/prx/libSceNpManager/include/Api.hpp index 11a6828eb8..ea1416018f 100644 --- a/core/libs/prx/libSceNpManager/include/Api.hpp +++ b/core/libs/prx/libSceNpManager/include/Api.hpp @@ -62,13 +62,13 @@ int APS5_VABI sceNpGetAccountLanguageA(int reqId, int userId, void* language); int APS5_VABI sceNpGetGamePresenceStatus(const NpOnlineId* onlineId, uint32_t* status); int APS5_VABI sceNpGetGamePresenceStatusA(int userId, uint32_t* status); int APS5_VABI sceNpGetParentalControlInfoA(int reqId, int userId, int8_t* age, void* info); -int APS5_VABI sceNpIdMapperAbortRequest(void); +int APS5_VABI sceNpIdMapperAbortRequest(int reqId); int APS5_VABI sceNpIdMapperAccountIdToNpId(void); int APS5_VABI sceNpIdMapperAccountIdToOnlineId(void); int APS5_VABI sceNpIdMapperCreateRequest(void); -int APS5_VABI sceNpIdMapperDeleteRequest(void); -int APS5_VABI sceNpIdMapperNpIdToAccountId(void); -int APS5_VABI sceNpIdMapperOnlineIdToAccountId(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 sceNpInGameMessageCheckCallback(void); int APS5_VABI sceNpInGameMessageGetMemoryPoolStatistics(void); @@ -81,7 +81,7 @@ int APS5_VABI sceNpIntGetNpTitleIdSecret(void); int APS5_VABI sceNpIntRegisterCheckCallback(void); int APS5_VABI sceNpIntRegisterGamePresenceCallback(void); int APS5_VABI sceNpIsPlusMember(int, bool* isMember); -int APS5_VABI sceNpManagerIntAbortRequest(void); +int APS5_VABI sceNpManagerIntAbortRequest(int reqId); int APS5_VABI sceNpManagerIntAddActiveSigninStateCallback(void); int APS5_VABI sceNpManagerIntAddOnlineIdChangeCallback(void); int APS5_VABI sceNpManagerIntAddPlusMemberTypeCallback(void); @@ -102,7 +102,7 @@ int APS5_VABI sceNpManagerIntCreateLoginContext(void); int APS5_VABI sceNpManagerIntCreateLoginRequest(void); int APS5_VABI sceNpManagerIntCreateRequest(void); int APS5_VABI sceNpManagerIntDeleteLoginContext(void); -int APS5_VABI sceNpManagerIntDeleteRequest(void); +int APS5_VABI sceNpManagerIntDeleteRequest(int reqId); int APS5_VABI sceNpManagerIntGetAccessTokenViaImplicitFlow(void); int APS5_VABI sceNpManagerIntGetAccountAge(void); int APS5_VABI sceNpManagerIntGetAccountCountry(void); diff --git a/core/libs/prx/libSceNpParty/Export.cpp b/core/libs/prx/libSceNpParty/Export.cpp index 4f00ce3699..971b894470 100644 --- a/core/libs/prx/libSceNpParty/Export.cpp +++ b/core/libs/prx/libSceNpParty/Export.cpp @@ -49,9 +49,8 @@ int APS5_VABI sceNpPartyGetId(void* id) { return notInParty(id); } -int APS5_VABI sceNpPartyGetMemberInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpPartyGetMemberInfo(std::uint16_t, void* memberInfo) { + return notInParty(memberInfo); } int APS5_VABI sceNpPartyGetMemberInfoA(std::uint16_t, void* memberInfo) { @@ -66,9 +65,8 @@ int APS5_VABI sceNpPartyGetMemberVoiceInfo(std::uint16_t, std::uint16_t, std::ui return notInParty(memberVoiceState); } -int APS5_VABI sceNpPartyGetMembers(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpPartyGetMembers(void* memberList) { + return notInParty(memberList); } int APS5_VABI sceNpPartyGetMembersA(void* memberList) { @@ -106,17 +104,16 @@ int APS5_VABI sceNpPartyLeave(void) { return 0; } -int APS5_VABI sceNpPartyRegisterHandler(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpPartyRegisterHandlerA(const void* handlers, void*) { if (!g_initialized) return NP_PARTY_ERROR_NOT_INITIALIZED; if (!handlers) return NP_PARTY_ERROR_INVALID_ARGUMENT; return 0; } +int APS5_VABI sceNpPartyRegisterHandler(const void* handlers, void* userdata) { + return sceNpPartyRegisterHandlerA(handlers, userdata); +} + int APS5_VABI sceNpPartyRegisterPrivateHandler(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceNpParty/include/Api.hpp b/core/libs/prx/libSceNpParty/include/Api.hpp index 6ac3a20bc7..8c8f33c8eb 100644 --- a/core/libs/prx/libSceNpParty/include/Api.hpp +++ b/core/libs/prx/libSceNpParty/include/Api.hpp @@ -9,11 +9,11 @@ int APS5_VABI sceNpPartyCheckCallback(void); int APS5_VABI sceNpPartyCreate(void); int APS5_VABI sceNpPartyCreateA(void); int APS5_VABI sceNpPartyGetId(void* id); -int APS5_VABI sceNpPartyGetMemberInfo(void); +int APS5_VABI sceNpPartyGetMemberInfo(std::uint16_t, void* memberInfo); int APS5_VABI sceNpPartyGetMemberInfoA(std::uint16_t, void* memberInfo); int APS5_VABI sceNpPartyGetMemberSessionInfo(std::uint16_t, void* memberGameSessionInfo); int APS5_VABI sceNpPartyGetMemberVoiceInfo(std::uint16_t, std::uint16_t, std::uint8_t* memberVoiceState); -int APS5_VABI sceNpPartyGetMembers(void); +int APS5_VABI sceNpPartyGetMembers(void* memberList); int APS5_VABI sceNpPartyGetMembersA(void* memberList); int APS5_VABI sceNpPartyGetStateAsUser(void); int APS5_VABI sceNpPartyGetStateAsUserA(std::int32_t, std::uint16_t* state); @@ -21,7 +21,7 @@ int APS5_VABI sceNpPartyGetVoiceChatPriority(void); int APS5_VABI sceNpPartyInitialize(const void* param); int APS5_VABI sceNpPartyJoin(void); int APS5_VABI sceNpPartyLeave(void); -int APS5_VABI sceNpPartyRegisterHandler(void); +int APS5_VABI sceNpPartyRegisterHandler(const void* handlers, void* userdata); int APS5_VABI sceNpPartyRegisterHandlerA(const void* handlers, void*); int APS5_VABI sceNpPartyRegisterPrivateHandler(void); int APS5_VABI sceNpPartySendBinaryMessage(const void* sendParams); diff --git a/core/libs/prx/libSceNpSignaling/Export.cpp b/core/libs/prx/libSceNpSignaling/Export.cpp index 1e4d40dcb1..00f307beb5 100644 --- a/core/libs/prx/libSceNpSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSignaling/Export.cpp @@ -162,9 +162,12 @@ int APS5_VABI sceNpSignalingTerminateConnection(int ctxId, int connId) { return SCE_NP_SIGNALING_ERROR_CONN_NOT_FOUND; } -int APS5_VABI sceNpSignalingDeactivateConnection(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpSignalingDeactivateConnection(int ctxId, int connId) { + (void)connId; + std::lock_guard lock(signalingMutex); + if (!initialized) return SCE_NP_SIGNALING_ERROR_NOT_INITIALIZED; + if (!FindContext(ctxId)) return SCE_NP_SIGNALING_ERROR_CTX_NOT_FOUND; + return SCE_NP_SIGNALING_ERROR_CONN_NOT_FOUND; } } diff --git a/core/libs/prx/libSceNpSignaling/include/Api.hpp b/core/libs/prx/libSceNpSignaling/include/Api.hpp index 1488e7e3da..d11753e6d8 100644 --- a/core/libs/prx/libSceNpSignaling/include/Api.hpp +++ b/core/libs/prx/libSceNpSignaling/include/Api.hpp @@ -12,6 +12,6 @@ int APS5_VABI sceNpSignalingInitialize(std::int64_t memorySize, int threadPriori int APS5_VABI sceNpSignalingSetContextOption(int ctxId, int optionId, int optionValue); int APS5_VABI sceNpSignalingTerminate(); int APS5_VABI sceNpSignalingTerminateConnection(int ctxId, int connId); -int APS5_VABI sceNpSignalingDeactivateConnection(void); +int APS5_VABI sceNpSignalingDeactivateConnection(int ctxId, int connId); } diff --git a/core/libs/prx/libSceNpTrophy/Export.cpp b/core/libs/prx/libSceNpTrophy/Export.cpp index 80fa87ee3f..85960c527c 100644 --- a/core/libs/prx/libSceNpTrophy/Export.cpp +++ b/core/libs/prx/libSceNpTrophy/Export.cpp @@ -18,6 +18,7 @@ namespace { constexpr int NP_TROPHY_ERROR_INVALID_ARGUMENT = static_cast(0x80551604u); constexpr int NP_TROPHY_ERROR_INSUFFICIENT_BUFFER = static_cast(0x80551605u); +constexpr int NP_TROPHY_ERROR_ABORT = static_cast(0x80551607u); constexpr int NP_TROPHY_ERROR_INVALID_HANDLE = static_cast(0x80551608u); constexpr int NP_TROPHY_ERROR_INVALID_CONTEXT = static_cast(0x80551609u); constexpr int NP_TROPHY_ERROR_INVALID_TROPHY_ID = static_cast(0x8055160au); @@ -69,6 +70,7 @@ struct Context { std::mutex g_mutex; std::map g_contexts; std::set g_handles; +std::set g_abortedHandles; int g_nextContext = 1; int g_nextHandle = 1; @@ -125,6 +127,11 @@ int Lookup(int contextId, int handle, Context*& context) { return 0; } +int LookupLive(int contextId, int handle, Context*& context) { + if (const int error = Lookup(contextId, handle, context)) return error; + return g_abortedHandles.contains(handle) ? NP_TROPHY_ERROR_ABORT : 0; +} + int Groups(const Context& context) { int groups = 0; for (int id = 0; id < NP_TROPHY_NUM_MAX; ++id) { @@ -187,14 +194,22 @@ int APS5_VABI sceNpTrophyCreateHandle(int* handle) { int APS5_VABI sceNpTrophyDestroyHandle(int handle) { std::lock_guard lock(g_mutex); + g_abortedHandles.erase(handle); return g_handles.erase(handle) == 1 ? 0 : NP_TROPHY_ERROR_INVALID_HANDLE; } +int APS5_VABI sceNpTrophyAbortHandle(int handle) { + std::lock_guard lock(g_mutex); + if (!g_handles.contains(handle)) return NP_TROPHY_ERROR_INVALID_HANDLE; + g_abortedHandles.insert(handle); + return 0; +} + int APS5_VABI sceNpTrophyRegisterContext(int contextId, int handle, std::uint64_t options) { if (options != 0) return NP_TROPHY_ERROR_INVALID_ARGUMENT; std::lock_guard lock(g_mutex); Context* context = nullptr; - if (const int error = Lookup(contextId, handle, context)) return error; + if (const int error = LookupLive(contextId, handle, context)) return error; if (context->registered) return NP_TROPHY_ERROR_ALREADY_REGISTERED; char name[32]; std::snprintf(name, sizeof(name), "trophy%02u.trp", context->serviceLabel); @@ -220,7 +235,7 @@ int APS5_VABI sceNpTrophyGetTrophyUnlockState(int contextId, int handle, TrophyF if (!flags || !count) return NP_TROPHY_ERROR_INVALID_ARGUMENT; std::lock_guard lock(g_mutex); Context* context = nullptr; - if (const int error = Lookup(contextId, handle, context)) return error; + if (const int error = LookupLive(contextId, handle, context)) return error; if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; std::memset(flags, 0, sizeof(*flags)); for (const auto id : context->unlocked) flags->bits[id >> 5] |= 1u << (id & 31); @@ -232,7 +247,7 @@ int APS5_VABI sceNpTrophyUnlockTrophy(int contextId, int handle, int trophyId, i if (!platinumId) return NP_TROPHY_ERROR_INVALID_ARGUMENT; std::lock_guard lock(g_mutex); Context* context = nullptr; - if (const int error = Lookup(contextId, handle, context)) return error; + if (const int error = LookupLive(contextId, handle, context)) return error; if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; if (trophyId < 0 || trophyId >= context->trophies) return NP_TROPHY_ERROR_INVALID_TROPHY_ID; if (context->unlocked.contains(trophyId)) return NP_TROPHY_ERROR_TROPHY_ALREADY_UNLOCKED; @@ -246,7 +261,7 @@ int APS5_VABI sceNpTrophyGetGameIcon(int contextId, int handle, void* buffer, st if (!size) return NP_TROPHY_ERROR_INVALID_ARGUMENT; std::lock_guard lock(g_mutex); Context* context = nullptr; - if (const int error = Lookup(contextId, handle, context)) return error; + if (const int error = LookupLive(contextId, handle, context)) return error; if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; return CopyIcon(*context, "ICON0.PNG", buffer, size); } @@ -255,7 +270,7 @@ int APS5_VABI sceNpTrophyGetGroupIcon(int contextId, int handle, int groupId, vo if (!size) return NP_TROPHY_ERROR_INVALID_ARGUMENT; std::lock_guard lock(g_mutex); Context* context = nullptr; - if (const int error = Lookup(contextId, handle, context)) return error; + if (const int error = LookupLive(contextId, handle, context)) return error; if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; if (groupId < 0) return NP_TROPHY_ERROR_INVALID_GROUP_ID; char icon[16]; @@ -267,7 +282,7 @@ int APS5_VABI sceNpTrophyGetTrophyIcon(int contextId, int handle, int trophyId, if (!size) return NP_TROPHY_ERROR_INVALID_ARGUMENT; std::lock_guard lock(g_mutex); Context* context = nullptr; - if (const int error = Lookup(contextId, handle, context)) return error; + if (const int error = LookupLive(contextId, handle, context)) return error; if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; if (trophyId < 0 || trophyId >= context->trophies) return NP_TROPHY_ERROR_INVALID_TROPHY_ID; if (!context->unlocked.contains(trophyId)) return NP_TROPHY_ERROR_TROPHY_NOT_UNLOCKED; @@ -281,7 +296,7 @@ int APS5_VABI sceNpTrophyGetGameInfo(int contextId, int handle, GameDetails* det if ((details && details->size != sizeof(GameDetails)) || (data && data->size != sizeof(GameData))) return NP_TROPHY_ERROR_INVALID_ARGUMENT; std::lock_guard lock(g_mutex); Context* context = nullptr; - if (const int error = Lookup(contextId, handle, context)) return error; + if (const int error = LookupLive(contextId, handle, context)) return error; if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; if (details) { std::memset(reinterpret_cast(details) + sizeof(details->size), 0, sizeof(GameDetails) - sizeof(details->size)); @@ -301,7 +316,7 @@ int APS5_VABI sceNpTrophyGetGroupInfo(int contextId, int handle, int groupId, Gr if ((details && details->size != sizeof(GroupDetails)) || (data && data->size != sizeof(GroupData))) return NP_TROPHY_ERROR_INVALID_ARGUMENT; std::lock_guard lock(g_mutex); Context* context = nullptr; - if (const int error = Lookup(contextId, handle, context)) return error; + if (const int error = LookupLive(contextId, handle, context)) return error; if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; if (groupId != NP_TROPHY_BASE_GAME_GROUP_ID) { if (groupId < 0 || groupId >= Groups(*context)) return NP_TROPHY_ERROR_INVALID_GROUP_ID; @@ -326,7 +341,7 @@ int APS5_VABI sceNpTrophyGetTrophyInfo(int contextId, int handle, int trophyId, if ((details && details->size != sizeof(TrophyDetails)) || (data && data->size != sizeof(TrophyData))) return NP_TROPHY_ERROR_INVALID_ARGUMENT; std::lock_guard lock(g_mutex); Context* context = nullptr; - if (const int error = Lookup(contextId, handle, context)) return error; + if (const int error = LookupLive(contextId, handle, context)) return error; if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; if (trophyId < 0 || trophyId >= context->trophies) return NP_TROPHY_ERROR_INVALID_TROPHY_ID; if (Groups(*context) != 0) NotImplemented_nid_no_patch(__func__); diff --git a/core/libs/prx/libSceNpTrophy/include/Api.hpp b/core/libs/prx/libSceNpTrophy/include/Api.hpp index 6360184288..19339aa673 100644 --- a/core/libs/prx/libSceNpTrophy/include/Api.hpp +++ b/core/libs/prx/libSceNpTrophy/include/Api.hpp @@ -79,6 +79,7 @@ int APS5_VABI sceNpTrophyCreateContext(int* context, int userId, std::uint32_t s int APS5_VABI sceNpTrophyDestroyContext(int context); int APS5_VABI sceNpTrophyCreateHandle(int* handle); int APS5_VABI sceNpTrophyDestroyHandle(int handle); +int APS5_VABI sceNpTrophyAbortHandle(int handle); int APS5_VABI sceNpTrophyRegisterContext(int contextId, int handle, std::uint64_t options); int APS5_VABI sceNpTrophyGetTrophyUnlockState(int contextId, int handle, TrophyFlagArray* flags, std::uint32_t* count); int APS5_VABI sceNpTrophyUnlockTrophy(int contextId, int handle, int trophyId, int* platinumId); diff --git a/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp b/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp index 4be4238929..f493e67ea1 100644 --- a/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp @@ -89,17 +89,27 @@ struct TrophyPackage { TrophyPackage ParseTrophyPackage(std::string_view ucp, std::string_view language); -enum class StatType { UInt32, UInt64 }; +enum class StatType { UInt32, UInt64, Int32 }; + +enum class StatAggregation { Latest, Max }; struct StatDefinition { StatType type = StatType::UInt64; + StatAggregation aggregation = StatAggregation::Latest; std::uint64_t minimum = 0; std::uint64_t maximum = UINT64_MAX; + std::uint64_t initial = 0; +}; + +struct StatFilter { + std::string path; + std::uint64_t value = 0; }; struct StatRule { std::string input; std::uint64_t statId = 0; + std::optional filter; }; struct StatRules { diff --git a/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp b/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp index d9ab341ae0..c69e678ba3 100644 --- a/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp @@ -67,6 +67,7 @@ public: explicit StatStore(std::filesystem::path directory); void Set(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId, std::uint64_t value); + std::uint64_t Raise(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId, std::uint64_t value); std::optional Value(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId); std::map Values(std::int32_t userId, std::uint32_t serviceLabel); std::filesystem::path RecordPath(std::int32_t userId, std::uint32_t serviceLabel) const; diff --git a/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp b/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp index 813fa576f7..91fb5e816e 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp @@ -139,6 +139,10 @@ StatChanges ApplyStatRules(const StatRules& rules, const NpTrophy2Event& event, const auto matching = rules.events.find(event.name); if (matching == rules.events.end()) return changes; for (const auto& rule : matching->second) { + if (rule.filter) { + const auto filtered = std::find_if(event.properties.begin(), event.properties.end(), [&rule](const NpTrophy2EventProperty& candidate) { return candidate.path == rule.filter->path; }); + if (filtered == event.properties.end() || filtered->value != rule.filter->value) continue; + } const auto& definition = rules.stats.at(rule.statId); const auto property = std::find_if(event.properties.begin(), event.properties.end(), [&rule](const NpTrophy2EventProperty& candidate) { return candidate.path == rule.input; }); const auto where = event.name + " $." + rule.input + " for stat " + std::to_string(rule.statId); @@ -148,6 +152,8 @@ StatChanges ApplyStatRules(const StatRules& rules, const NpTrophy2Event& event, changes.problems.push_back({where, property->text + " is not an unsigned integer"}); } else if (*property->value < definition.minimum || *property->value > definition.maximum) { changes.problems.push_back({where, property->text + " is not from " + std::to_string(definition.minimum) + " to " + std::to_string(definition.maximum)}); + } else if (definition.aggregation == StatAggregation::Max) { + changes.updates.push_back({rule.statId, stats.Raise(userId, serviceLabel, rule.statId, std::max(*property->value, definition.initial)), rule.input}); } else { stats.Set(userId, serviceLabel, rule.statId, *property->value); changes.updates.push_back({rule.statId, *property->value, rule.input}); diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp index 5da39d1cb4..91140de5cf 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp @@ -363,26 +363,34 @@ void readStat(const Json& definition, std::size_t index, StatRules& rules) { } const auto name = "stat " + std::to_string(*statId); const auto* aggregation = definition.Find("aggregation"); - if (aggregation == nullptr || aggregation->kind != Json::Kind::String || aggregation->text != "latest") { - rules.skipped.push_back(name + " has aggregation " + (aggregation != nullptr ? describe(*aggregation) : std::string("none")) + "; only \"latest\" is supported"); + const auto named = [](const Json* value, std::string_view text) { return value != nullptr && value->kind == Json::Kind::String && value->text == text; }; + StatDefinition stat; + if (named(aggregation, "latest")) { + stat.aggregation = StatAggregation::Latest; + } else if (named(aggregation, "max")) { + stat.aggregation = StatAggregation::Max; + } else { + rules.skipped.push_back(name + " has aggregation " + (aggregation != nullptr ? describe(*aggregation) : std::string("none")) + "; only \"latest\" and \"max\" are supported"); return; } const auto* type = definition.Find("dataType"); - StatDefinition stat; - if (type != nullptr && type->kind == Json::Kind::String && type->text == "uint32") { + if (named(type, "uint32")) { stat.type = StatType::UInt32; stat.maximum = UINT32_MAX; - } else if (type != nullptr && type->kind == Json::Kind::String && type->text == "uint64") { + } else if (named(type, "uint64")) { stat.type = StatType::UInt64; stat.maximum = UINT64_MAX; + } else if (named(type, "int32")) { + stat.type = StatType::Int32; + stat.maximum = INT32_MAX; } else { - rules.skipped.push_back(name + " has dataType " + (type != nullptr ? describe(*type) : std::string("none")) + "; only \"uint32\" and \"uint64\" are supported"); + rules.skipped.push_back(name + " has dataType " + (type != nullptr ? describe(*type) : std::string("none")) + "; only \"uint32\", \"uint64\" and \"int32\" are supported"); return; } if (const auto* minimum = definition.Find("minValue")) { const auto value = minimum->Unsigned(); if (!value || *value > stat.maximum) { - rules.skipped.push_back(name + " has minValue " + describe(*minimum) + ", which its " + type->text + " type cannot hold"); + rules.skipped.push_back(name + " has minValue " + describe(*minimum) + (stat.type == StatType::Int32 ? ", which is not from 0 to its int32 limit" : ", which its " + type->text + " type cannot hold")); return; } stat.minimum = *value; @@ -395,6 +403,15 @@ void readStat(const Json& definition, std::size_t index, StatRules& rules) { } stat.maximum = *value; } + stat.initial = stat.minimum; + if (const auto* initial = definition.Find("initialValue"); initial != nullptr && stat.aggregation == StatAggregation::Max) { + const auto value = initial->Unsigned(); + if (!value || *value < stat.minimum || *value > stat.maximum) { + rules.skipped.push_back(name + " has initialValue " + describe(*initial) + ", which is not from its minValue to its maxValue"); + return; + } + stat.initial = *value; + } if (!rules.stats.emplace(*statId, stat).second) rules.skipped.push_back(name + " is defined twice; the first definition is used"); } @@ -411,9 +428,20 @@ void readRule(const Json& rule, std::size_t index, StatRules& rules) { rules.skipped.push_back(where + " has no eventName"); return; } + std::optional filter; if (condition->members.size() != 1) { - rules.skipped.push_back(where + " filters " + event->text + " by more than its eventName; only eventName conditions are supported"); - return; + const auto* property = condition->Find("property"); + const auto* path = property != nullptr && property->kind == Json::Kind::Object ? property->Find("path") : nullptr; + const auto* comparator = property != nullptr && property->kind == Json::Kind::Object ? property->Find("comparator") : nullptr; + const auto* value = property != nullptr && property->kind == Json::Kind::Object ? property->Find("value") : nullptr; + const auto filterPath = path != nullptr && path->kind == Json::Kind::String ? propertyPath(path->text) : std::nullopt; + const auto filterValue = value != nullptr ? value->Unsigned() : std::nullopt; + const bool equal = comparator != nullptr && comparator->kind == Json::Kind::String && comparator->text == "=="; + if (condition->members.size() != 2 || property == nullptr || property->kind != Json::Kind::Object || property->members.size() != 3 || !filterPath || !equal || !filterValue) { + rules.skipped.push_back(where + " filters " + event->text + " by more than its eventName; only an eventName with one \"$.name\" property equal to an unsigned integer is supported"); + return; + } + filter = StatFilter{*filterPath, *filterValue}; } const auto* output = action->Find("output"); const auto* id = output != nullptr ? output->Find("statId") : nullptr; @@ -432,7 +460,7 @@ void readRule(const Json& rule, std::size_t index, StatRules& rules) { rules.skipped.push_back(where + " for " + event->text + " reads " + (input != nullptr ? describe(*input) : std::string("no input")) + "; only \"$.name\" property paths are supported"); return; } - rules.events[event->text].push_back({*path, *statId}); + rules.events[event->text].push_back({*path, *statId, std::move(filter)}); } } diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp index b19804fe10..5091aad105 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp @@ -177,6 +177,18 @@ void StatStore::Set(std::int32_t userId, std::uint32_t serviceLabel, std::uint64 stats = std::move(updated); } +std::uint64_t StatStore::Raise(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId, std::uint64_t value) { + std::lock_guard lock(mutex); + const Key key{userId, serviceLabel}; + auto& stats = record(key); + if (const auto found = stats.find(statId); found != stats.end() && found->second >= value) return found->second; + auto updated = stats; + updated[statId] = value; + writeRecord(directory, RecordPath(userId, serviceLabel), updated, "stat record"); + stats = std::move(updated); + return value; +} + std::optional StatStore::Value(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId) { std::lock_guard lock(mutex); const auto& stats = record({userId, serviceLabel}); diff --git a/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp b/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp index d1dcdba734..0e7b0e4364 100644 --- a/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp +++ b/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp @@ -94,7 +94,11 @@ const char StatsDefinition[] = R"({"statDefinitionArray":[ {"statId":50,"dataType":"uint64","aggregation":"sum"}, {"statId":60,"dataType":"int32","aggregation":"latest"}, {"statId":70,"dataType":"uint32","aggregation":"latest","maxValue":"5000000000"}, - {"dataType":"uint32","aggregation":"latest"} + {"dataType":"uint32","aggregation":"latest"}, + {"statId":80,"dataType":"int32","aggregation":"max","minValue":"0","initialValue":"0"}, + {"statId":90,"dataType":"uint32","aggregation":"max","initialValue":"3"}, + {"statId":100,"dataType":"int32","aggregation":"max","initialValue":"-1"}, + {"statId":110,"dataType":"int32","aggregation":"latest","minValue":"-5"} ]})"; const char StatsExtraction[] = R"({"statsExtractionRuleArray":[ @@ -106,7 +110,12 @@ const char StatsExtraction[] = R"({"statsExtractionRuleArray":[ {"condition":{"eventName":"FILTERED","property":{"path":"$.level"}},"action":{"input":"$.counter","output":{"statId":10}}}, {"condition":{"eventName":"COUNT"},"action":{"output":{"statId":10}}}, {"condition":{"eventName":"BRACKETS"},"action":{"input":"$['counter']","output":{"statId":10}}}, - {"bad":1} + {"bad":1}, + {"condition":{"eventName":"PROGRESS","property":{"path":"$._trophy_id","comparator":"==","value":"5"}},"action":{"input":"$._trophy_progress","output":{"statId":80}}}, + {"condition":{"eventName":"PROGRESS","property":{"path":"$._trophy_id","comparator":"==","value":6}},"action":{"input":"$._trophy_progress","output":{"statId":90}}}, + {"condition":{"eventName":"RANGED","property":{"path":"$._trophy_id","comparator":">=","value":"5"}},"action":{"input":"$._trophy_progress","output":{"statId":80}}}, + {"condition":{"eventName":"NAMED","property":{"path":"$.mode","comparator":"==","value":"delta"}},"action":{"input":"$._trophy_progress","output":{"statId":80}}}, + {"condition":{"eventName":"EXTRA","property":{"path":"$._trophy_id","comparator":"==","value":"5"},"other":1},"action":{"input":"$._trophy_progress","output":{"statId":80}}} ]})"; std::string trophyPackage() { @@ -310,10 +319,13 @@ void testProgressAndPlatinum() { void testStatRules() { const auto rules = NpTrophy2::ParseStatRules(statPackage()); - check(rules.stats.size() == 4 && rules.stats.at(10).type == NpTrophy2::StatType::UInt64 && rules.stats.at(30).type == NpTrophy2::StatType::UInt32 && rules.stats.at(30).maximum == UINT32_MAX && - rules.stats.at(40).maximum == 50 && rules.stats.at(10).minimum == 0, + check(rules.stats.size() == 7 && rules.stats.at(10).type == NpTrophy2::StatType::UInt64 && rules.stats.at(30).type == NpTrophy2::StatType::UInt32 && rules.stats.at(30).maximum == UINT32_MAX && + rules.stats.at(40).maximum == 50 && rules.stats.at(10).minimum == 0 && rules.stats.at(10).aggregation == NpTrophy2::StatAggregation::Latest, "stat definitions were not read"); - check(rules.events.size() == 3 && rules.events.at("COLLECT").size() == 2 && rules.events.at("COLLECT")[1].input == "total" && rules.events.at("COLLECT")[1].statId == 20 && + check(rules.stats.at(60).type == NpTrophy2::StatType::Int32 && rules.stats.at(60).maximum == INT32_MAX && rules.stats.at(80).aggregation == NpTrophy2::StatAggregation::Max && + rules.stats.at(80).type == NpTrophy2::StatType::Int32 && rules.stats.at(80).initial == 0 && rules.stats.at(90).aggregation == NpTrophy2::StatAggregation::Max && rules.stats.at(90).initial == 3, + "int32 and max stat definitions were not read"); + check(rules.events.size() == 4 && rules.events.at("COLLECT").size() == 2 && rules.events.at("COLLECT")[1].input == "total" && rules.events.at("COLLECT")[1].statId == 20 && rules.events.at("STEPS")[0].input == "player.steps" && rules.events.at("TIME")[0].statId == 30, "extraction rules were not read"); std::set skipped(rules.skipped.begin(), rules.skipped.end()); @@ -323,11 +335,17 @@ void testStatRules() { } return false; }; - check(rules.skipped.size() == 9, "unsupported definitions and rules were not all reported"); - check(reported("stat 50 has aggregation \"sum\"") && reported("stat 60 has dataType \"int32\"") && reported("stat 70 has maxValue \"5000000000\"") && reported("statDefinitionArray[7] has no unsigned statId"), + check(rules.skipped.size() == 13, "unsupported definitions and rules were not all reported"); + check(reported("stat 50 has aggregation \"sum\"; only \"latest\" and \"max\" are supported") && reported("stat 70 has maxValue \"5000000000\"") && reported("statDefinitionArray[7] has no unsigned statId") && + reported("stat 100 has initialValue \"-1\"") && reported("stat 110 has minValue \"-5\", which is not from 0 to its int32 limit"), "unsupported definitions were not reported"); - check(reported("for SUM writes stat 50") && reported("filters FILTERED") && reported("for COUNT reads no input") && reported("for BRACKETS reads \"$['counter']\"") && reported("[8] has no condition"), + check(reported("for SUM writes stat 50") && reported("filters FILTERED") && reported("for COUNT reads no input") && reported("for BRACKETS reads \"$['counter']\"") && reported("[8] has no condition") && + reported("filters RANGED") && reported("filters NAMED") && reported("filters EXTRA"), "unsupported rules were not reported"); + const auto& progress = rules.events.at("PROGRESS"); + check(progress.size() == 2 && progress[0].filter && progress[0].filter->path == "_trophy_id" && progress[0].filter->value == 5 && progress[0].input == "_trophy_progress" && progress[0].statId == 80 && + progress[1].filter && progress[1].filter->value == 6 && progress[1].statId == 90 && !rules.events.at("COLLECT")[0].filter, + "property filters were not read"); for (const char* input : {"$", "$.", "$..a", "$.a.", "$.a-b", "counter", "$.a b"}) { const std::string extraction = std::string(R"({"statsExtractionRuleArray":[{"condition":{"eventName":"E"},"action":{"input":")") + input + R"(","output":{"statId":10}}}]})"; check(NpTrophy2::ParseStatRules(ucp({{"stats_definition.json", StatsDefinition}, {"stats_extraction.json", extraction}})).events.empty(), "an unsupported input path was accepted"); @@ -412,6 +430,37 @@ void testApplyStatRules() { check(latest.updates.size() == 2 && stats.Value(InitialUser, 0, 10) == std::optional(2), "a stat did not keep the latest value"); } +void testProgressRules() { + const auto rules = NpTrophy2::ParseStatRules(statPackage()); + NpTrophy2::StatStore stats("progress"); + const auto posted = [&](std::vector properties) { return NpTrophy2::ApplyStatRules(rules, event("PROGRESS", std::move(properties)), stats, InitialUser, 0); }; + const auto progress = [&](std::uint64_t trophy, std::uint64_t value) { return posted({{"_trophy_id", trophy, std::to_string(trophy)}, {"_trophy_progress", value, std::to_string(value)}}); }; + const auto first = progress(5, 4); + check(first.problems.empty() && first.updates.size() == 1 && first.updates[0].statId == 80 && first.updates[0].value == 4 && stats.Value(InitialUser, 0, 80) == std::optional(4) && + !stats.Value(InitialUser, 0, 90), + "a filtered rule did not take its trophy's progress, or another trophy's rule did"); + const auto lower = progress(5, 2); + check(lower.updates.size() == 1 && lower.updates[0].value == 4 && stats.Value(InitialUser, 0, 80) == std::optional(4), "a max stat went down"); + const auto higher = progress(5, 7); + check(higher.updates.size() == 1 && higher.updates[0].value == 7 && stats.Value(InitialUser, 0, 80) == std::optional(7), "a max stat did not rise"); + const auto floor = progress(6, 1); + check(floor.updates.size() == 1 && floor.updates[0].statId == 90 && floor.updates[0].value == 3 && stats.Value(InitialUser, 0, 90) == std::optional(3), "a max stat started below its initialValue"); + const auto signedMax = progress(5, 2147483648ull); + check(signedMax.updates.empty() && signedMax.problems.size() == 1 && signedMax.problems[0].reason == "2147483648 is not from 0 to 2147483647", "an int32 stat took a value it cannot hold"); + const auto other = progress(7, 9); + const auto unnamed = posted({{"_trophy_progress", 9, "9"}}); + const auto negative = posted({{"_trophy_id", std::nullopt, "-5"}, {"_trophy_progress", 9, "9"}}); + check(other.updates.empty() && other.problems.empty() && unnamed.updates.empty() && unnamed.problems.empty() && negative.updates.empty() && negative.problems.empty() && + stats.Value(InitialUser, 0, 80) == std::optional(7) && stats.Value(InitialUser, 0, 90) == std::optional(3), + "a filtered rule took an event for another trophy, or without its trophy id"); + const auto missing = posted({{"_trophy_id", 5, "5"}}); + check(missing.updates.empty() && missing.problems.size() == 1 && missing.problems[0].rule == "PROGRESS $._trophy_progress for stat 80", "a matching event without its input was not reported"); + NpTrophy2::StatStore restarted("progress"); + check(restarted.Raise(InitialUser, 0, 80, 6) == 7 && restarted.Raise(InitialUser, 0, 80, 8) == 8 && restarted.Raise(InitialUser, 0, 120, 1) == 1, "a raise did not keep the larger value"); + NpTrophy2::StatStore reread("progress"); + check(reread.Value(InitialUser, 0, 80) == std::optional(8) && reread.Value(InitialUser, 0, 120) == std::optional(1), "a raised stat was not stored"); +} + } int main() { @@ -427,6 +476,7 @@ int main() { testConditions(); testStatStore(); testApplyStatRules(); + testProgressRules(); std::filesystem::current_path(previous); std::filesystem::remove_all(root); std::puts("Trophy package tests passed"); diff --git a/core/libs/prx/libSceNpTus/Export.cpp b/core/libs/prx/libSceNpTus/Export.cpp index 6bf0782927..2f632e6d18 100644 --- a/core/libs/prx/libSceNpTus/Export.cpp +++ b/core/libs/prx/libSceNpTus/Export.cpp @@ -347,11 +347,6 @@ int APS5_VABI sceNpTusGetDataForCrossSaveVUserAsync(void) { return 0; } -int APS5_VABI sceNpTusGetMultiUserVariableForCrossSave(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpTusGetFriendsVariableA(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -694,6 +689,10 @@ int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveAsync(int reqId, const std return StartOffline(reqId); } +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSave(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserVariableForCrossSaveAsync(reqId, targetAccountIdArray, slotId, variableArray, variableArraySize, arrayLen, option)); +} + int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { (void)variableArraySize; if (!targetVirtualUserIdArray || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; @@ -1007,44 +1006,36 @@ int APS5_VABI sceNpTusCreateNpTitleCtx() { return 0; } -int APS5_VABI sceNpTusGetData() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusGetData(int reqId, const void* targetNpId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + return Complete(reqId, sceNpTusGetDataAsync(reqId, targetNpId, slotId, dataStatus, dataStatusSize, data, recvSize, option)); } -int APS5_VABI sceNpTusGetDataVUser() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusGetDataVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + return Complete(reqId, sceNpTusGetDataVUserAsync(reqId, targetVirtualUserId, slotId, dataStatus, dataStatusSize, data, recvSize, option)); } -int APS5_VABI sceNpTusGetMultiSlotVariable() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusGetMultiSlotVariable(int reqId, const void* targetNpId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotVariableAsync(reqId, targetNpId, slotIds, variableArray, variableArraySize, arrayLen, option)); } -int APS5_VABI sceNpTusGetMultiSlotVariableVUser() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusGetMultiSlotVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotVariableVUserAsync(reqId, targetVirtualUserId, slotIds, variableArray, variableArraySize, arrayLen, option)); } -int APS5_VABI sceNpTusSetData() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusSetData(int reqId, const void* targetNpId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const void* isLastChangedAuthor, const void* isLastChangedDate, void* option) { + return Complete(reqId, sceNpTusSetDataAsync(reqId, targetNpId, slotId, totalSize, sendSize, data, info, infoStructSize, isLastChangedAuthor, isLastChangedDate, option)); } -int APS5_VABI sceNpTusSetDataVUser() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusSetDataVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const void* isLastChangedAuthor, const void* isLastChangedDate, void* option) { + return Complete(reqId, sceNpTusSetDataVUserAsync(reqId, targetVirtualUserId, slotId, totalSize, sendSize, data, info, infoStructSize, isLastChangedAuthor, isLastChangedDate, option)); } -int APS5_VABI sceNpTusSetMultiSlotVariable() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusSetMultiSlotVariable(int reqId, const void* targetNpId, const std::int32_t* slotIds, const std::int64_t* variableArray, int arrayLen, void* option) { + return Complete(reqId, sceNpTusSetMultiSlotVariableAsync(reqId, targetNpId, slotIds, variableArray, arrayLen, option)); } -int APS5_VABI sceNpTusSetMultiSlotVariableVUser() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusSetMultiSlotVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, const std::int64_t* variableArray, int arrayLen, void* option) { + return Complete(reqId, sceNpTusSetMultiSlotVariableVUserAsync(reqId, targetVirtualUserId, slotIds, variableArray, arrayLen, option)); } } diff --git a/core/libs/prx/libSceNpTus/include/Api.hpp b/core/libs/prx/libSceNpTus/include/Api.hpp index b4e6807d5e..eacd0fe74a 100644 --- a/core/libs/prx/libSceNpTus/include/Api.hpp +++ b/core/libs/prx/libSceNpTus/include/Api.hpp @@ -45,7 +45,7 @@ int APS5_VABI sceNpTssGetStorageAsync(void); int APS5_VABI sceNpTssGetDataAsync(void); int APS5_VABI sceNpTusGetDataForCrossSaveVUser(void); int APS5_VABI sceNpTusGetDataForCrossSaveVUserAsync(void); -int APS5_VABI sceNpTusGetMultiUserVariableForCrossSave(void); +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSave(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); int APS5_VABI sceNpTusGetFriendsVariableA(void); int APS5_VABI sceNpTusGetFriendsVariableAAsync(void); int APS5_VABI sceNpTusGetFriendsVariableForCrossSave(void); @@ -138,13 +138,13 @@ int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUser(int reqId, const NpOnli int APS5_VABI sceNpTusTryAndSetVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); int APS5_VABI sceNpTssGetData(); int APS5_VABI sceNpTusCreateNpTitleCtx(); -int APS5_VABI sceNpTusGetData(); -int APS5_VABI sceNpTusGetDataVUser(); -int APS5_VABI sceNpTusGetMultiSlotVariable(); -int APS5_VABI sceNpTusGetMultiSlotVariableVUser(); -int APS5_VABI sceNpTusSetData(); -int APS5_VABI sceNpTusSetDataVUser(); -int APS5_VABI sceNpTusSetMultiSlotVariable(); -int APS5_VABI sceNpTusSetMultiSlotVariableVUser(); +int APS5_VABI sceNpTusGetData(int reqId, const void* targetNpId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetMultiSlotVariable(int reqId, const void* targetNpId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusSetData(int reqId, const void* targetNpId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const void* isLastChangedAuthor, const void* isLastChangedDate, void* option); +int APS5_VABI sceNpTusSetDataVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const void* isLastChangedAuthor, const void* isLastChangedDate, void* option); +int APS5_VABI sceNpTusSetMultiSlotVariable(int reqId, const void* targetNpId, const std::int32_t* slotIds, const std::int64_t* variableArray, int arrayLen, void* option); +int APS5_VABI sceNpTusSetMultiSlotVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, const std::int64_t* variableArray, int arrayLen, void* option); } diff --git a/core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp b/core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp index 942a5f9450..12bcbd69c4 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp @@ -5,8 +5,6 @@ #include #include #include -#include -#include #include #include @@ -22,13 +20,6 @@ Uds::Owned& events() { return registry; } -bool firstProgress(std::int32_t trophyId) { - static std::mutex mutex; - static std::set logged; - std::lock_guard lock(mutex); - return logged.insert(trophyId).second; -} - void ignore(const NpUniversalDataSystemEvent& event, int context, const std::string& reason) { std::fprintf(stderr, "[trophy] ignored %s posted to context %d: %s\n", Uds::Describe(event).c_str(), context, reason.c_str()); } @@ -39,14 +30,12 @@ void post(const NpUniversalDataSystemEvent& event, int context) { const auto trophy = Uds::RecognizeTrophyEvent(event); switch (trophy.kind) { case Uds::TrophyEventKind::None: + case Uds::TrophyEventKind::Progress: if (const auto owner = Uds::FindContext(context)) NpTrophy2ApplyEvent_nid_no_patch(owner->userId, owner->serviceLabel, Uds::StatEvent(event)); return; case Uds::TrophyEventKind::Malformed: ignore(event, context, trophy.problem); return; - case Uds::TrophyEventKind::Progress: - if (firstProgress(trophy.trophyId)) ignore(event, context, "trophy progress is not recorded"); - return; case Uds::TrophyEventKind::Unlock: if (const auto owner = Uds::FindContext(context)) NpTrophy2RecordUnlock_nid_no_patch(owner->userId, owner->serviceLabel, trophy.trophyId); else ignore(event, context, "the context does not exist"); diff --git a/core/libs/prx/libSceNpUtility/Export.cpp b/core/libs/prx/libSceNpUtility/Export.cpp index 7f5e3bbf94..364f0db20c 100644 --- a/core/libs/prx/libSceNpUtility/Export.cpp +++ b/core/libs/prx/libSceNpUtility/Export.cpp @@ -1,6 +1,7 @@ #include "prx/libSceNpUtility/include/Api.hpp" #include #include +#include #include #include #include @@ -16,6 +17,7 @@ static constexpr int SCE_NP_COMMUNITY_ERROR_ABORTED = static_cast(0x8055070 static constexpr int SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT = static_cast(0x8055070C); static constexpr int SCE_NP_COMMUNITY_ERROR_INVALID_ID = static_cast(0x8055070E); static constexpr int SCE_NP_COMMUNITY_ERROR_INVALID_TYPE = static_cast(0x80550711); +static constexpr int SCE_NP_LOOKUP_ERROR_SYNCHRONOUS_REQUEST = static_cast(0x80550307); static constexpr int POLL_FINISHED = 0; static constexpr int POLL_RUNNING = 1; static constexpr std::size_t MAX_TITLE_CONTEXTS = 32; @@ -23,7 +25,9 @@ static constexpr std::size_t MAX_REQUESTS = 32; static constexpr int WORD_FILTER_TITLE_CTX_ID_OFFSET = 0x01000000; static constexpr int WORD_FILTER_REQUEST_ID_OFFSET = 0x02000000; static constexpr std::size_t WORD_FILTER_COMMENT_MAX_LENGTH = 0x400; -static constexpr std::uint64_t WORD_FILTER_ASYNC_PARAMETER_SIZE = 0x18; +static constexpr std::uint64_t ASYNC_REQUEST_PARAMETER_SIZE = 0x18; +static constexpr std::uint32_t RESOLVE_TIMEOUT_MIN = 1000000; +static constexpr std::uint32_t TRANSFER_TIMEOUT_MIN = 10000000; namespace { struct Request { @@ -38,6 +42,7 @@ struct Request { struct Service { Service(int firstTitleContext, int firstRequest) : nextTitleContext(firstTitleContext), nextRequest(firstRequest) {} std::mutex mutex; + std::condition_variable finished; std::unordered_set titleContexts; std::unordered_map requests; int nextTitleContext; @@ -60,6 +65,7 @@ int DeleteTitleContext(Service& service, int titleCtxId) { std::lock_guard lock(service.mutex); if (!service.titleContexts.erase(titleCtxId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; std::erase_if(service.requests, [titleCtxId](const auto& entry) { return entry.second.titleCtxId == titleCtxId; }); + service.finished.notify_all(); return 0; } @@ -74,7 +80,9 @@ int CreateRequest(Service& service, int titleCtxId, bool async) { int DeleteRequest(Service& service, int reqId) { std::lock_guard lock(service.mutex); - return service.requests.erase(reqId) ? 0 : SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (!service.requests.erase(reqId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + service.finished.notify_all(); + return 0; } int StartOffline(Service& service, int reqId) { @@ -86,9 +94,62 @@ int StartOffline(Service& service, int reqId) { request->second.used = true; request->second.finished = true; request->second.result = SCE_NP_ERROR_SIGNED_OUT; + service.finished.notify_all(); return request->second.async ? 0 : SCE_NP_ERROR_SIGNED_OUT; } +int AbortRequest(Service& service, int reqId) { + std::lock_guard lock(service.mutex); + const auto request = service.requests.find(reqId); + if (request == service.requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + request->second.aborted = true; + if (!request->second.finished) { + request->second.finished = true; + request->second.result = SCE_NP_COMMUNITY_ERROR_ABORTED; + service.finished.notify_all(); + } + return 0; +} + +int PollAsync(Service& service, int reqId, int* result, int synchronousError) { + if (!result) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(service.mutex); + const auto request = service.requests.find(reqId); + if (request == service.requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (!request->second.async) return synchronousError; + if (!request->second.finished) return POLL_RUNNING; + *result = request->second.result; + return POLL_FINISHED; +} + +int WaitAsync(Service& service, int reqId, int* result, int synchronousError) { + if (!result) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + std::unique_lock lock(service.mutex); + auto request = service.requests.find(reqId); + if (request == service.requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (!request->second.async) return synchronousError; + service.finished.wait(lock, [&] { + request = service.requests.find(reqId); + return request == service.requests.end() || request->second.finished; + }); + if (request == service.requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + *result = request->second.result; + return 0; +} + +bool ValidTimeout(std::uint32_t timeout, std::uint32_t minimum) { + return timeout == 0 || timeout >= minimum; +} + +int SetTimeout(Service& service, int id, int resolveRetry, std::uint32_t resolveTimeout, std::uint32_t connTimeout, std::uint32_t sendTimeout, std::uint32_t recvTimeout) { + if (resolveRetry < 0 || !ValidTimeout(resolveTimeout, RESOLVE_TIMEOUT_MIN) || !ValidTimeout(connTimeout, TRANSFER_TIMEOUT_MIN) || + !ValidTimeout(sendTimeout, TRANSFER_TIMEOUT_MIN) || !ValidTimeout(recvTimeout, TRANSFER_TIMEOUT_MIN)) + return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (resolveRetry == 0 && resolveTimeout == 0 && connTimeout == 0 && sendTimeout == 0 && recvTimeout == 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(service.mutex); + return service.titleContexts.contains(id) || service.requests.contains(id) ? 0 : SCE_NP_COMMUNITY_ERROR_INVALID_ID; +} + int CheckComment(const char* comment) { return strnlen(comment, WORD_FILTER_COMMENT_MAX_LENGTH + 1) > WORD_FILTER_COMMENT_MAX_LENGTH ? SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT : 0; } @@ -234,16 +295,7 @@ int APS5_VABI sceNpAppLaunchLinkIntInitialize() { return 0; } int APS5_VABI sceNpBandwidthTestShutdownWithDetailedInfo() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceNpLookupAbortRequest() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceNpLookupCreateAsyncRequest() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_NP_ERROR_INVALID_ARGUMENT; } int APS5_VABI sceNpLookupCreateTitleCtxA() { NotImplemented_nid_no_patch(__func__); @@ -313,18 +365,6 @@ int APS5_VABI sceNpLookupNetTerm() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpLookupPollAsync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceNpLookupSetTimeout() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceNpLookupWaitAsync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} int APS5_VABI sceNpServiceChecker2IntAbortRequest() { NotImplemented_nid_no_patch(__func__); return 0; @@ -421,21 +461,21 @@ int APS5_VABI sceNpWordFilterCreateTitleCtxA() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpWordFilterSetTimeout() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceNpWordFilterWaitAsync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} } extern "C" { int APS5_VABI sceNpBandwidthTestInitStart() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_NP_ERROR_SIGNED_OUT; +} + +int APS5_VABI sceNpLookupAbortRequest(int reqId) { + return AbortRequest(lookup, reqId); +} + +int APS5_VABI sceNpLookupCreateAsyncRequest(int titleCtxId, const std::uint64_t* param) { + if (!param || *param != ASYNC_REQUEST_PARAMETER_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return CreateRequest(lookup, titleCtxId, true); } int APS5_VABI sceNpLookupCreateRequest(int titleCtxId) { @@ -460,16 +500,20 @@ int APS5_VABI sceNpLookupNpId(int reqId, const NpOnlineId* onlineId, void* npId, return StartOffline(lookup, reqId); } +int APS5_VABI sceNpLookupPollAsync(int reqId, int* result) { + return PollAsync(lookup, reqId, result, SCE_NP_LOOKUP_ERROR_SYNCHRONOUS_REQUEST); +} + +int APS5_VABI sceNpLookupSetTimeout(int id, int resolveRetry, std::uint32_t resolveTimeout, std::uint32_t connTimeout, std::uint32_t sendTimeout, std::uint32_t recvTimeout) { + return SetTimeout(lookup, id, resolveRetry, resolveTimeout, connTimeout, sendTimeout, recvTimeout); +} + +int APS5_VABI sceNpLookupWaitAsync(int reqId, int* result) { + return WaitAsync(lookup, reqId, result, SCE_NP_LOOKUP_ERROR_SYNCHRONOUS_REQUEST); +} + int APS5_VABI sceNpWordFilterAbortRequest(int reqId) { - std::lock_guard lock(wordFilter.mutex); - const auto request = wordFilter.requests.find(reqId); - if (request == wordFilter.requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; - request->second.aborted = true; - if (!request->second.finished) { - request->second.finished = true; - request->second.result = SCE_NP_COMMUNITY_ERROR_ABORTED; - } - return 0; + return AbortRequest(wordFilter, reqId); } int APS5_VABI sceNpWordFilterCensorComment(int reqId, const char* comment, void* option) { @@ -480,7 +524,7 @@ int APS5_VABI sceNpWordFilterCensorComment(int reqId, const char* comment, void* } int APS5_VABI sceNpWordFilterCreateAsyncRequest(int titleCtxId, const std::uint64_t* param) { - if (!param || *param != WORD_FILTER_ASYNC_PARAMETER_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!param || *param != ASYNC_REQUEST_PARAMETER_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; return CreateRequest(wordFilter, titleCtxId, true); } @@ -501,13 +545,7 @@ int APS5_VABI sceNpWordFilterDeleteTitleCtx(int titleCtxId) { } int APS5_VABI sceNpWordFilterPollAsync(int reqId, int* result) { - if (!result) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - std::lock_guard lock(wordFilter.mutex); - const auto request = wordFilter.requests.find(reqId); - if (request == wordFilter.requests.end() || !request->second.async) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; - if (!request->second.finished) return POLL_RUNNING; - *result = request->second.result; - return POLL_FINISHED; + return PollAsync(wordFilter, reqId, result, SCE_NP_COMMUNITY_ERROR_INVALID_ID); } int APS5_VABI sceNpWordFilterSanitizeComment(int reqId, const char* comment, char* sanitizedComment, void* option) { @@ -517,4 +555,12 @@ int APS5_VABI sceNpWordFilterSanitizeComment(int reqId, const char* comment, cha return StartOffline(wordFilter, reqId); } +int APS5_VABI sceNpWordFilterSetTimeout(int id, int resolveRetry, std::uint32_t resolveTimeout, std::uint32_t connTimeout, std::uint32_t sendTimeout, std::uint32_t recvTimeout) { + return SetTimeout(wordFilter, id, resolveRetry, resolveTimeout, connTimeout, sendTimeout, recvTimeout); +} + +int APS5_VABI sceNpWordFilterWaitAsync(int reqId, int* result) { + return WaitAsync(wordFilter, reqId, result, SCE_NP_COMMUNITY_ERROR_INVALID_ID); +} + } diff --git a/core/libs/prx/libSceNpUtility/include/Api.hpp b/core/libs/prx/libSceNpUtility/include/Api.hpp index 73d099655b..0fe054556f 100644 --- a/core/libs/prx/libSceNpUtility/include/Api.hpp +++ b/core/libs/prx/libSceNpUtility/include/Api.hpp @@ -23,6 +23,13 @@ int APS5_VABI sceNpWordFilterCreateTitleCtx(const void* selfNpId); int APS5_VABI sceNpWordFilterDeleteRequest(int reqId); int APS5_VABI sceNpWordFilterDeleteTitleCtx(int titleCtxId); int APS5_VABI sceNpWordFilterPollAsync(int reqId, int* result); +int APS5_VABI sceNpLookupAbortRequest(int reqId); +int APS5_VABI sceNpLookupCreateAsyncRequest(int titleCtxId, const std::uint64_t* param); +int APS5_VABI sceNpLookupPollAsync(int reqId, int* result); +int APS5_VABI sceNpLookupSetTimeout(int id, int resolveRetry, std::uint32_t resolveTimeout, std::uint32_t connTimeout, std::uint32_t sendTimeout, std::uint32_t recvTimeout); +int APS5_VABI sceNpLookupWaitAsync(int reqId, int* result); +int APS5_VABI sceNpWordFilterSetTimeout(int id, int resolveRetry, std::uint32_t resolveTimeout, std::uint32_t connTimeout, std::uint32_t sendTimeout, std::uint32_t recvTimeout); +int APS5_VABI sceNpWordFilterWaitAsync(int reqId, int* result); int APS5_VABI sceNpWordFilterSanitizeComment(int reqId, const char* comment, char* sanitizedComment, void* option); int APS5_VABI sceNpAppInfoIntAbortRequest(); @@ -53,8 +60,6 @@ int APS5_VABI sceNpAppLaunchLinkIntGetCompatibleTitleIdList(); int APS5_VABI sceNpAppLaunchLinkIntGetCompatibleTitleIdNum(); int APS5_VABI sceNpAppLaunchLinkIntInitialize(); int APS5_VABI sceNpBandwidthTestShutdownWithDetailedInfo(); -int APS5_VABI sceNpLookupAbortRequest(); -int APS5_VABI sceNpLookupCreateAsyncRequest(); int APS5_VABI sceNpLookupCreateTitleCtxA(); int APS5_VABI sceNpLookupNetAbortRequest(); int APS5_VABI sceNpLookupNetCensorComment(); @@ -72,9 +77,6 @@ int APS5_VABI sceNpLookupNetNpId(); int APS5_VABI sceNpLookupNetSanitizeComment(); int APS5_VABI sceNpLookupNetSetTimeout(); int APS5_VABI sceNpLookupNetTerm(); -int APS5_VABI sceNpLookupPollAsync(); -int APS5_VABI sceNpLookupSetTimeout(); -int APS5_VABI sceNpLookupWaitAsync(); int APS5_VABI sceNpServiceChecker2IntAbortRequest(); int APS5_VABI sceNpServiceChecker2IntCreateRequest(); int APS5_VABI sceNpServiceChecker2IntDestroyRequest(); @@ -99,7 +101,5 @@ int APS5_VABI sceNpTitleMetadataIntGetNpTitleId(); int APS5_VABI sceNpUtilityInit(); int APS5_VABI sceNpUtilityTerm(); int APS5_VABI sceNpWordFilterCreateTitleCtxA(); -int APS5_VABI sceNpWordFilterSetTimeout(); -int APS5_VABI sceNpWordFilterWaitAsync(); } diff --git a/core/libs/prx/libSceNpWebApi/Export.cpp b/core/libs/prx/libSceNpWebApi/Export.cpp index c78414d680..269bbec537 100644 --- a/core/libs/prx/libSceNpWebApi/Export.cpp +++ b/core/libs/prx/libSceNpWebApi/Export.cpp @@ -1,51 +1,133 @@ #include "prx/libSceNpWebApi/include/Api.hpp" #include "SceTypes.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceUserService/UserService.hpp" #include #include +#include #include +#include #include #include static constexpr int SCE_NP_WEBAPI_ERROR_INVALID_ARGUMENT = static_cast(0x80552902); +static constexpr int SCE_NP_WEBAPI_ERROR_INVALID_LIB_CONTEXT_ID = static_cast(0x80552903); static constexpr int SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND = static_cast(0x80552904); +static constexpr int SCE_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND = static_cast(0x80552905); +static constexpr int SCE_NP_WEBAPI_ERROR_REQUEST_NOT_FOUND = static_cast(0x80552906); +static constexpr int SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN = static_cast(0x80552907); +static constexpr int SCE_NP_WEBAPI_ERROR_INVALID_CONTENT_PARAMETER = static_cast(0x80552908); +static constexpr int SCE_NP_WEBAPI_ERROR_ABORTED = static_cast(0x80552909); +static constexpr int SCE_NP_WEBAPI_ERROR_USER_CONTEXT_ALREADY_EXIST = static_cast(0x8055290A); static constexpr int SCE_NP_WEBAPI_ERROR_PUSH_EVENT_FILTER_NOT_FOUND = static_cast(0x8055290B); +static constexpr int SCE_NP_WEBAPI_ERROR_PUSH_EVENT_CALLBACK_NOT_FOUND = static_cast(0x8055290C); static constexpr int SCE_NP_WEBAPI_ERROR_HANDLE_NOT_FOUND = static_cast(0x8055290D); static constexpr int SCE_NP_WEBAPI_ERROR_SERVICE_PUSH_EVENT_FILTER_NOT_FOUND = static_cast(0x8055290E); +static constexpr int SCE_NP_WEBAPI_ERROR_SERVICE_PUSH_EVENT_CALLBACK_NOT_FOUND = static_cast(0x8055290F); static constexpr int SCE_NP_WEBAPI_ERROR_SIGNED_IN_USER_NOT_FOUND = static_cast(0x80552910); static constexpr int SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_MAX = static_cast(0x8055291A); +static constexpr int SCE_NP_WEBAPI_HTTP_METHOD_DELETE = 3; +static constexpr std::uint32_t SCE_NP_INVALID_SERVICE_LABEL = 0xFFFFFFFF; +static constexpr std::uint32_t SDK_VERSION_1_00 = 0x01000000; +static constexpr std::uint32_t SDK_VERSION_2_00 = 0x02000000; +static constexpr std::uint32_t SDK_VERSION_2_50 = 0x02500000; + namespace { +struct Request { + bool aborted; +}; + +struct UserContext { + int userId; + std::unordered_map requests; + std::unordered_set pushEventCallbacks; + std::unordered_set servicePushEventCallbacks; +}; + struct LibraryContext { int libHttpCtxId; std::size_t poolSize; std::unordered_set handles; std::unordered_set pushEventFilters; + std::unordered_map userContexts; +}; + +struct UserContextLookup { + int error; + LibraryContext* library; + UserContext* user; +}; + +struct RequestLookup { + int error; + UserContext* user; + Request* request; }; std::mutex contextsMutex; std::unordered_map contexts; int lastContext = 0; int lastObject = 0; +int lastUserContext = 0; +std::int64_t lastRequest = 0; int nextObject() { lastObject = lastObject % 0xefffffff + 1; return lastObject; } +std::uint32_t compiledSdkVersion() { + const auto* process = static_cast(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t version; + std::memcpy(&version, process + 16, sizeof(version)); + return version; +} + +UserContextLookup findUserContext(int titleUserCtxId) { + const auto library = contexts.find(titleUserCtxId >> 16); + if (library == contexts.end()) return {SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND, nullptr, nullptr}; + const auto user = library->second.userContexts.find(titleUserCtxId); + if (user == library->second.userContexts.end()) return {SCE_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND, &library->second, nullptr}; + return {0, &library->second, &user->second}; +} + +RequestLookup findRequest(std::int64_t requestId) { + const auto lookup = findUserContext(static_cast(requestId >> 32)); + if (lookup.error != 0) return {lookup.error, nullptr, nullptr}; + const auto request = lookup.user->requests.find(requestId); + if (request == lookup.user->requests.end()) return {SCE_NP_WEBAPI_ERROR_REQUEST_NOT_FOUND, lookup.user, nullptr}; + return {0, lookup.user, &request->second}; +} + } extern "C" { -int APS5_VABI sceNpWebApiAbortRequest() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpWebApiAbortRequest(std::int64_t requestId) { + std::lock_guard lock(contextsMutex); + const auto lookup = findRequest(requestId); + if (lookup.error != 0) return lookup.error; + lookup.request->aborted = true; return 0; } -int APS5_VABI sceNpWebApiCreateContext() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpWebApiCreateContext(int libCtxId, const void* onlineId) { + if (libCtxId >= 0x8000) return SCE_NP_WEBAPI_ERROR_INVALID_LIB_CONTEXT_ID; + if (!onlineId) return SCE_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(contextsMutex); + const auto context = contexts.find(libCtxId); + if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + auto& userContexts = context->second.userContexts; + for (const auto& [id, user] : userContexts) { + if (user.userId == USER_SERVICE_INITIAL_USER_ID) return SCE_NP_WEBAPI_ERROR_USER_CONTEXT_ALREADY_EXIST; + } + lastUserContext = lastUserContext % 0xffff + 1; + const int titleUserCtxId = (libCtxId << 16) | lastUserContext; + userContexts.emplace(titleUserCtxId, UserContext{USER_SERVICE_INITIAL_USER_ID, {}, {}, {}}); + return titleUserCtxId; } int APS5_VABI sceNpWebApiCreateHandle(int libCtxId) { @@ -67,14 +149,26 @@ int APS5_VABI sceNpWebApiCreatePushEventFilter(int libCtxId, const void* filterP return filter; } -int APS5_VABI sceNpWebApiCreateRequest() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpWebApiCreateRequest(int titleUserCtxId, const char* apiGroup, const char* path, int method, const NpWebApiContentParameter* contentParameter, std::int64_t* requestId) { + if (!apiGroup || !path) return SCE_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + if (contentParameter && contentParameter->contentLength != 0 && !contentParameter->contentType) return SCE_NP_WEBAPI_ERROR_INVALID_CONTENT_PARAMETER; + if (method > SCE_NP_WEBAPI_HTTP_METHOD_DELETE && compiledSdkVersion() >= SDK_VERSION_2_50) return SCE_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(contextsMutex); + const auto lookup = findUserContext(titleUserCtxId); + if (lookup.error != 0) return lookup.error; + lastRequest = lastRequest % 0xffffffff + 1; + const std::int64_t base = static_cast(titleUserCtxId) << 32; + std::int64_t id = base | lastRequest; + while (lookup.user->requests.contains(id)) --id; + if (id <= base) throw std::runtime_error("sceNpWebApiCreateRequest: every request id of the user context is in use"); + lookup.user->requests.emplace(id, Request{false}); + if (requestId) *requestId = id; return 0; } int APS5_VABI sceNpWebApiCreateServicePushEventFilter(int libCtxId, int handleId, const char* npServiceName, std::uint32_t npServiceLabel, const void* filterParam, std::size_t filterParamNum) { - (void)npServiceLabel; if (!npServiceName || !filterParam || filterParamNum == 0) return SCE_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + if (npServiceLabel == SCE_NP_INVALID_SERVICE_LABEL && compiledSdkVersion() >= SDK_VERSION_2_00) return SCE_NP_WEBAPI_ERROR_INVALID_ARGUMENT; std::lock_guard lock(contextsMutex); const auto context = contexts.find(libCtxId); if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; @@ -82,8 +176,11 @@ int APS5_VABI sceNpWebApiCreateServicePushEventFilter(int libCtxId, int handleId return SCE_NP_WEBAPI_ERROR_SIGNED_IN_USER_NOT_FOUND; } -int APS5_VABI sceNpWebApiDeleteContext() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpWebApiDeleteContext(int titleUserCtxId) { + std::lock_guard lock(contextsMutex); + const auto lookup = findUserContext(titleUserCtxId); + if (lookup.error != 0) return lookup.error; + lookup.library->userContexts.erase(titleUserCtxId); return 0; } @@ -101,8 +198,11 @@ int APS5_VABI sceNpWebApiDeletePushEventFilter(int libCtxId, int filterId) { return context->second.pushEventFilters.erase(filterId) != 0 ? 0 : SCE_NP_WEBAPI_ERROR_PUSH_EVENT_FILTER_NOT_FOUND; } -int APS5_VABI sceNpWebApiDeleteRequest() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpWebApiDeleteRequest(std::int64_t requestId) { + std::lock_guard lock(contextsMutex); + const auto lookup = findRequest(requestId); + if (lookup.error != 0) return lookup.error; + lookup.user->requests.erase(requestId); return 0; } @@ -133,7 +233,7 @@ int APS5_VABI sceNpWebApiInitialize(int libHttpCtxId, std::size_t poolSize) { for (int attempt = 0; attempt < 0x7fff; ++attempt) { lastContext = lastContext % 0x7fff + 1; if (contexts.contains(lastContext)) continue; - contexts[lastContext] = {libHttpCtxId, poolSize, {}, {}}; + contexts[lastContext] = {libHttpCtxId, poolSize, {}, {}, {}}; return lastContext; } return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_MAX; @@ -144,19 +244,40 @@ int APS5_VABI sceNpWebApiReadData() { return 0; } -int APS5_VABI sceNpWebApiRegisterPushEventCallback() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpWebApiRegisterPushEventCallback(int titleUserCtxId, int filterId, void* callback, void* userArg) { + (void)userArg; + if (!callback && compiledSdkVersion() >= SDK_VERSION_1_00) return SCE_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(contextsMutex); + const auto lookup = findUserContext(titleUserCtxId); + if (lookup.error != 0) return lookup.error; + if (!lookup.library->pushEventFilters.contains(filterId) && compiledSdkVersion() >= SDK_VERSION_2_50) return SCE_NP_WEBAPI_ERROR_PUSH_EVENT_FILTER_NOT_FOUND; + const auto callbackId = nextObject(); + lookup.user->pushEventCallbacks.insert(callbackId); + return callbackId; } -int APS5_VABI sceNpWebApiRegisterServicePushEventCallback() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpWebApiRegisterServicePushEventCallback(int titleUserCtxId, int filterId, void* callback, void* userArg) { + (void)filterId; + (void)userArg; + if (!callback && compiledSdkVersion() >= SDK_VERSION_1_00) return SCE_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(contextsMutex); + const auto lookup = findUserContext(titleUserCtxId); + if (lookup.error != 0) return lookup.error; + if (compiledSdkVersion() >= SDK_VERSION_2_50) return SCE_NP_WEBAPI_ERROR_SERVICE_PUSH_EVENT_FILTER_NOT_FOUND; + if (!callback) return SCE_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + const auto callbackId = nextObject(); + lookup.user->servicePushEventCallbacks.insert(callbackId); + return callbackId; } -int APS5_VABI sceNpWebApiSendRequest() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpWebApiSendRequest(std::int64_t requestId, const void* data, std::size_t dataSize) { + (void)data; + (void)dataSize; + std::lock_guard lock(contextsMutex); + const auto lookup = findRequest(requestId); + if (lookup.error != 0) return lookup.error; + if (lookup.request->aborted) return SCE_NP_WEBAPI_ERROR_ABORTED; + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; } int APS5_VABI sceNpWebApiTerminate(int libCtxId) { @@ -164,14 +285,18 @@ int APS5_VABI sceNpWebApiTerminate(int libCtxId) { return contexts.erase(libCtxId) != 0 ? 0 : SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; } -int APS5_VABI sceNpWebApiUnregisterPushEventCallback() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpWebApiUnregisterPushEventCallback(int titleUserCtxId, int callbackId) { + std::lock_guard lock(contextsMutex); + const auto lookup = findUserContext(titleUserCtxId); + if (lookup.error != 0) return lookup.error; + return lookup.user->pushEventCallbacks.erase(callbackId) != 0 ? 0 : SCE_NP_WEBAPI_ERROR_PUSH_EVENT_CALLBACK_NOT_FOUND; } -int APS5_VABI sceNpWebApiUnregisterServicePushEventCallback() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpWebApiUnregisterServicePushEventCallback(int titleUserCtxId, int callbackId) { + std::lock_guard lock(contextsMutex); + const auto lookup = findUserContext(titleUserCtxId); + if (lookup.error != 0) return lookup.error; + return lookup.user->servicePushEventCallbacks.erase(callbackId) != 0 ? 0 : SCE_NP_WEBAPI_ERROR_SERVICE_PUSH_EVENT_CALLBACK_NOT_FOUND; } int APS5_VABI sceNpWebApiUtilityParseNpId() { diff --git a/core/libs/prx/libSceNpWebApi/include/Api.hpp b/core/libs/prx/libSceNpWebApi/include/Api.hpp index 567364a9bf..ec9ce3c836 100644 --- a/core/libs/prx/libSceNpWebApi/include/Api.hpp +++ b/core/libs/prx/libSceNpWebApi/include/Api.hpp @@ -2,30 +2,38 @@ #include "SceTypes.hpp" +struct NpWebApiContentParameter { + std::uint64_t contentLength; + const char* contentType; + std::uint8_t reserved[16]; +}; + +static_assert(sizeof(NpWebApiContentParameter) == 32); + extern "C" { -int APS5_VABI sceNpWebApiAbortRequest(); -int APS5_VABI sceNpWebApiCreateContext(); +int APS5_VABI sceNpWebApiAbortRequest(std::int64_t requestId); +int APS5_VABI sceNpWebApiCreateContext(int libCtxId, const void* onlineId); int APS5_VABI sceNpWebApiCreateHandle(int libCtxId); int APS5_VABI sceNpWebApiCreatePushEventFilter(int libCtxId, const void* filterParam, std::size_t filterParamNum); -int APS5_VABI sceNpWebApiCreateRequest(); +int APS5_VABI sceNpWebApiCreateRequest(int titleUserCtxId, const char* apiGroup, const char* path, int method, const NpWebApiContentParameter* contentParameter, std::int64_t* requestId); int APS5_VABI sceNpWebApiCreateServicePushEventFilter(int libCtxId, int handleId, const char* npServiceName, std::uint32_t npServiceLabel, const void* filterParam, std::size_t filterParamNum); -int APS5_VABI sceNpWebApiDeleteContext(); +int APS5_VABI sceNpWebApiDeleteContext(int titleUserCtxId); int APS5_VABI sceNpWebApiDeleteHandle(int libCtxId, int handleId); int APS5_VABI sceNpWebApiDeletePushEventFilter(int libCtxId, int filterId); -int APS5_VABI sceNpWebApiDeleteRequest(); +int APS5_VABI sceNpWebApiDeleteRequest(std::int64_t requestId); int APS5_VABI sceNpWebApiDeleteServicePushEventFilter(int libCtxId, int filterId); int APS5_VABI sceNpWebApiGetHttpResponseHeaderValue(); int APS5_VABI sceNpWebApiGetHttpResponseHeaderValueLength(); int APS5_VABI sceNpWebApiGetHttpStatusCode(); int APS5_VABI sceNpWebApiInitialize(int libHttpCtxId, std::size_t poolSize); int APS5_VABI sceNpWebApiReadData(); -int APS5_VABI sceNpWebApiRegisterPushEventCallback(); -int APS5_VABI sceNpWebApiRegisterServicePushEventCallback(); -int APS5_VABI sceNpWebApiSendRequest(); +int APS5_VABI sceNpWebApiRegisterPushEventCallback(int titleUserCtxId, int filterId, void* callback, void* userArg); +int APS5_VABI sceNpWebApiRegisterServicePushEventCallback(int titleUserCtxId, int filterId, void* callback, void* userArg); +int APS5_VABI sceNpWebApiSendRequest(std::int64_t requestId, const void* data, std::size_t dataSize); int APS5_VABI sceNpWebApiTerminate(int libCtxId); -int APS5_VABI sceNpWebApiUnregisterPushEventCallback(); -int APS5_VABI sceNpWebApiUnregisterServicePushEventCallback(); +int APS5_VABI sceNpWebApiUnregisterPushEventCallback(int titleUserCtxId, int callbackId); +int APS5_VABI sceNpWebApiUnregisterServicePushEventCallback(int titleUserCtxId, int callbackId); int APS5_VABI sceNpWebApiUtilityParseNpId(); } diff --git a/core/libs/prx/libSceNpWebApi2/Export.cpp b/core/libs/prx/libSceNpWebApi2/Export.cpp index 66a26fe70b..9731e4cb0b 100644 --- a/core/libs/prx/libSceNpWebApi2/Export.cpp +++ b/core/libs/prx/libSceNpWebApi2/Export.cpp @@ -207,7 +207,7 @@ int APS5_VABI sceNpWebApi2PushEventSetHandleTimeout(int libraryContextId, int ha return 0; } -int APS5_VABI sceNpWebApi2PushEventRegisterPushContextCallback() { +int APS5_VABI sceNpWebApi2PushEventRegisterPushContextCallback(std::int32_t userContextId, std::int32_t filterId, void* callback, void* userArg) { return g_nextHandle.fetch_add(1, std::memory_order_relaxed); } diff --git a/core/libs/prx/libSceNpWebApi2/include/Api.hpp b/core/libs/prx/libSceNpWebApi2/include/Api.hpp index c528b23c20..bc63c54826 100644 --- a/core/libs/prx/libSceNpWebApi2/include/Api.hpp +++ b/core/libs/prx/libSceNpWebApi2/include/Api.hpp @@ -39,7 +39,7 @@ int APS5_VABI sceNpWebApi2Terminate(int libCtxId); int APS5_VABI sceNpWebApi2PushEventCreatePushContext(std::int32_t userContextId, NpWebApi2PushContextId* pushContextId); int APS5_VABI sceNpWebApi2PushEventDeleteFilter(std::int32_t libraryContextId, std::int32_t filterId); int APS5_VABI sceNpWebApi2PushEventSetHandleTimeout(int libraryContextId, int handleId, std::uint32_t timeout); -int APS5_VABI sceNpWebApi2PushEventRegisterPushContextCallback(); +int APS5_VABI sceNpWebApi2PushEventRegisterPushContextCallback(std::int32_t userContextId, std::int32_t filterId, void* callback, void* userArg); int APS5_VABI sceNpWebApi2PushEventStartPushContextCallback(std::int32_t userContextId, const NpWebApi2PushContextId* pushContextId); int APS5_VABI sceNpWebApi2PushEventUnregisterCallback(std::int32_t userContextId, std::int32_t callbackId); int APS5_VABI sceNpWebApi2PushEventUnregisterPushContextCallback(std::int32_t userContextId, std::int32_t callbackId); diff --git a/core/libs/prx/libSceOpusCeltDec/Export.cpp b/core/libs/prx/libSceOpusCeltDec/Export.cpp index 41ef6ba1a0..664483dd5d 100644 --- a/core/libs/prx/libSceOpusCeltDec/Export.cpp +++ b/core/libs/prx/libSceOpusCeltDec/Export.cpp @@ -1,6 +1,7 @@ #include "prx/libSceOpusCeltDec/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libSceOpusDec/include/OpusDecoder.hpp" +#include "prx/libSceOpusDec/include/OpusDecodeVariants.hpp" #include #include "prx/libc/include/General.hpp" @@ -8,6 +9,8 @@ namespace { OpusDecoderLibrary library("OpusCeltDec", true); +constexpr std::uint32_t ErrorBase = 0x81400000u; + } // namespace extern "C" { @@ -38,19 +41,16 @@ int APS5_VABI sceOpusCeltDecCtl() { return 0; } -int APS5_VABI sceOpusCeltDecDecodeFloat() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceOpusCeltDecDecodeFloat(void* state, const std::uint8_t* data, int bytes, float* pcm, int capacity) { + return OpusDecodeVariant(library, ErrorBase, state, data, bytes, pcm, capacity, OpusDefaultDecodePriority, true); } -int APS5_VABI sceOpusCeltDecDecodeFloatWithPriority() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceOpusCeltDecDecodeFloatWithPriority(void* state, const std::uint8_t* data, int bytes, float* pcm, int capacity, int priority) { + return OpusDecodeVariant(library, ErrorBase, state, data, bytes, pcm, capacity, priority, true); } -int APS5_VABI sceOpusCeltDecDecodeWithPriority() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceOpusCeltDecDecodeWithPriority(void* state, const std::uint8_t* data, int bytes, std::int16_t* pcm, int capacity, int priority) { + return OpusDecodeVariant(library, ErrorBase, state, data, bytes, pcm, capacity, priority, false); } } diff --git a/core/libs/prx/libSceOpusCeltDec/include/Api.hpp b/core/libs/prx/libSceOpusCeltDec/include/Api.hpp index 2cbb6f7599..1ba7cb6be1 100644 --- a/core/libs/prx/libSceOpusCeltDec/include/Api.hpp +++ b/core/libs/prx/libSceOpusCeltDec/include/Api.hpp @@ -12,8 +12,8 @@ int APS5_VABI sceOpusCeltDecDecode(void* state, const std::uint8_t* data, int by int APS5_VABI sceOpusCeltDecDestroy(void* state); int APS5_VABI sceOpusCeltDecCreate(); int APS5_VABI sceOpusCeltDecCtl(); -int APS5_VABI sceOpusCeltDecDecodeFloat(); -int APS5_VABI sceOpusCeltDecDecodeFloatWithPriority(); -int APS5_VABI sceOpusCeltDecDecodeWithPriority(); +int APS5_VABI sceOpusCeltDecDecodeFloat(void* state, const std::uint8_t* data, int bytes, float* pcm, int capacity); +int APS5_VABI sceOpusCeltDecDecodeFloatWithPriority(void* state, const std::uint8_t* data, int bytes, float* pcm, int capacity, int priority); +int APS5_VABI sceOpusCeltDecDecodeWithPriority(void* state, const std::uint8_t* data, int bytes, std::int16_t* pcm, int capacity, int priority); } diff --git a/core/libs/prx/libSceOpusDec/CMakeLists.txt b/core/libs/prx/libSceOpusDec/CMakeLists.txt index 01bf008dd3..4356e19cab 100644 --- a/core/libs/prx/libSceOpusDec/CMakeLists.txt +++ b/core/libs/prx/libSceOpusDec/CMakeLists.txt @@ -14,3 +14,15 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_opus_dec_variants_tests ${LIBS_INCLUDE_DIR}/tests/GuestOpusDecVariants.cpp) + target_include_directories(guest_opus_dec_variants_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_opus_dec_variants_tests PRIVATE libSceOpusDec libSceOpusCeltDec libc) + configure_windows_unwind(guest_opus_dec_variants_tests) + add_test(NAME guest_opus_dec_variants COMMAND guest_opus_dec_variants_tests) + if(WIN32) + set_tests_properties(guest_opus_dec_variants PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$;PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceOpusDec/Export.cpp b/core/libs/prx/libSceOpusDec/Export.cpp index f190172797..138675d8f7 100644 --- a/core/libs/prx/libSceOpusDec/Export.cpp +++ b/core/libs/prx/libSceOpusDec/Export.cpp @@ -1,6 +1,7 @@ #include "prx/libSceOpusDec/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libSceOpusDec/include/OpusDecoder.hpp" +#include "prx/libSceOpusDec/include/OpusDecodeVariants.hpp" #include #include "prx/libc/include/General.hpp" @@ -8,6 +9,8 @@ namespace { OpusDecoderLibrary library("OpusDec", false); +constexpr std::uint32_t ErrorBase = 0x81740000u; + } // namespace extern "C" { @@ -43,29 +46,26 @@ int APS5_VABI sceOpusDecCtl() { return 0; } -int APS5_VABI sceOpusDecDecodeFloat() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceOpusDecDecodeFloat(void* state, const std::uint8_t* data, int bytes, float* pcm, int capacity) { + return OpusDecodeVariant(library, ErrorBase, state, data, bytes, pcm, capacity, OpusDefaultDecodePriority, true); } -int APS5_VABI sceOpusDecDecodeFloatWithPriority() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceOpusDecDecodeFloatWithPriority(void* state, const std::uint8_t* data, int bytes, float* pcm, int capacity, int priority) { + return OpusDecodeVariant(library, ErrorBase, state, data, bytes, pcm, capacity, priority, true); } -int APS5_VABI sceOpusDecDecodeFloatWithPriorityAndTimeout() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceOpusDecDecodeFloatWithPriorityAndTimeout(void* state, const std::uint8_t* data, int bytes, float* pcm, int capacity, int priority, + std::uint32_t timeout, std::uint32_t*) { + return OpusDecodeVariantWithTimeout(library, ErrorBase, state, data, bytes, pcm, capacity, priority, true, timeout, __func__); } -int APS5_VABI sceOpusDecDecodeWithPriority() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceOpusDecDecodeWithPriority(void* state, const std::uint8_t* data, int bytes, std::int16_t* pcm, int capacity, int priority) { + return OpusDecodeVariant(library, ErrorBase, state, data, bytes, pcm, capacity, priority, false); } -int APS5_VABI sceOpusDecDecodeWithPriorityAndTimeout() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceOpusDecDecodeWithPriorityAndTimeout(void* state, const std::uint8_t* data, int bytes, std::int16_t* pcm, int capacity, int priority, + std::uint32_t timeout, std::uint32_t*) { + return OpusDecodeVariantWithTimeout(library, ErrorBase, state, data, bytes, pcm, capacity, priority, false, timeout, __func__); } } diff --git a/core/libs/prx/libSceOpusDec/include/Api.hpp b/core/libs/prx/libSceOpusDec/include/Api.hpp index 3bf7ca2885..492ffce7c0 100644 --- a/core/libs/prx/libSceOpusDec/include/Api.hpp +++ b/core/libs/prx/libSceOpusDec/include/Api.hpp @@ -13,10 +13,12 @@ int APS5_VABI sceOpusDecDestroy(void* state); int APS5_VABI sceOpusDecCancel(); int APS5_VABI sceOpusDecCreate(); int APS5_VABI sceOpusDecCtl(); -int APS5_VABI sceOpusDecDecodeFloat(); -int APS5_VABI sceOpusDecDecodeFloatWithPriority(); -int APS5_VABI sceOpusDecDecodeFloatWithPriorityAndTimeout(); -int APS5_VABI sceOpusDecDecodeWithPriority(); -int APS5_VABI sceOpusDecDecodeWithPriorityAndTimeout(); +int APS5_VABI sceOpusDecDecodeFloat(void* state, const std::uint8_t* data, int bytes, float* pcm, int capacity); +int APS5_VABI sceOpusDecDecodeFloatWithPriority(void* state, const std::uint8_t* data, int bytes, float* pcm, int capacity, int priority); +int APS5_VABI sceOpusDecDecodeFloatWithPriorityAndTimeout(void* state, const std::uint8_t* data, int bytes, float* pcm, int capacity, int priority, + std::uint32_t timeout, std::uint32_t* result); +int APS5_VABI sceOpusDecDecodeWithPriority(void* state, const std::uint8_t* data, int bytes, std::int16_t* pcm, int capacity, int priority); +int APS5_VABI sceOpusDecDecodeWithPriorityAndTimeout(void* state, const std::uint8_t* data, int bytes, std::int16_t* pcm, int capacity, int priority, + std::uint32_t timeout, std::uint32_t* result); } diff --git a/core/libs/prx/libSceOpusDec/include/OpusDecodeVariants.hpp b/core/libs/prx/libSceOpusDec/include/OpusDecodeVariants.hpp new file mode 100644 index 0000000000..b6bc7a3c33 --- /dev/null +++ b/core/libs/prx/libSceOpusDec/include/OpusDecodeVariants.hpp @@ -0,0 +1,58 @@ +#ifndef CORE_LIBS_PRX_LIBSCEOPUSDEC_INCLUDE_OPUSDECODEVARIANTS_HPP +#define CORE_LIBS_PRX_LIBSCEOPUSDEC_INCLUDE_OPUSDECODEVARIANTS_HPP + +#include +#include +#include +#include +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int OpusDefaultDecodePriority = 40; +constexpr std::uint32_t OpusInfiniteTimeout = 0xFFFFFFFFu; + +inline int OpusCheckVariant(std::uint32_t errorBase, const void* state, const std::uint8_t* data, int bytes, const void* pcm, int capacity, int priority) { + const auto error = [errorBase](std::uint32_t offset) { return static_cast(errorBase + offset); }; + if (!state) return error(0); + if (bytes != 0 && !data) return error(6); + if (bytes < 0) return error(7); + if (!pcm) return error(8); + if (capacity < 0) return error(9); + if (priority < 20 || priority > 100) return error(10); + return 0; +} + +template +int OpusDecodeVariant(TLibrary& library, std::uint32_t errorBase, void* state, const std::uint8_t* data, int bytes, void* pcm, + int capacity, int priority, bool asFloat) { + if (const int check = OpusCheckVariant(errorBase, state, data, bytes, pcm, capacity, priority)) return check; + auto* samples = static_cast(pcm); + if (!asFloat) return library.Decode(state, data, bytes, samples, capacity); + const int result = library.Decode(state, data, bytes, samples, capacity / 4 * 2); + if (result < 0) return result; + const auto count = static_cast(result / 2); + std::vector decoded(count); + std::memcpy(decoded.data(), pcm, count * 2); + auto* output = static_cast(pcm); + for (std::size_t index = 0; index < count; ++index) { + const float value = decoded[index] > 0 ? decoded[index] / 32767.0f : decoded[index] / 32768.0f; + std::memcpy(output + index * 4, &value, 4); + } + return result * 2; +} + +template +int OpusDecodeVariantWithTimeout(TLibrary& library, std::uint32_t errorBase, void* state, const std::uint8_t* data, int bytes, void* pcm, + int capacity, int priority, bool asFloat, std::uint32_t timeout, const char* name) { + if (timeout != OpusInfiniteTimeout) { + if (const int check = OpusCheckVariant(errorBase, state, data, bytes, pcm, capacity, priority)) return check; + NotImplemented_nid_no_patch(name); + return 0; + } + return OpusDecodeVariant(library, errorBase, state, data, bytes, pcm, capacity, priority, asFloat); +} + +} // namespace + +#endif diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 1a8ccb28a8..0787a07dbf 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -35,6 +35,9 @@ constexpr int PAD_ERROR_DEVICE_NOT_CONNECTED = static_cast(0x80920007); constexpr int PAD_ERROR_DEVICE_NO_HANDLE = static_cast(0x80920008); constexpr int PAD_ERROR_ALREADY_OPENED = static_cast(0x80920004); constexpr int PAD_ERROR_UNKNOWN_USER = static_cast(0x809b0081); +constexpr int PAD_ERROR_INVALID_USER_ID = static_cast(0x809b0001); +constexpr int PAD_ERROR_INVALID_PORT_INDEX = static_cast(0x809b0003); +constexpr int PAD_USER_ID_INVALID = -1; std::array g_opened{false}; std::array g_everOpened{false}; @@ -258,9 +261,12 @@ int APS5_VABI scePadInit_nid_postfix(void) { int APS5_VABI scePadOpen_nid_postfix(int userId, int type, int index, const void* param) { (void)param; - if (!ValidPort(userId, type, index)) { - return PAD_ERROR_INVALID_ARG; - } + const bool unknownType = type == 3 || type == 4; + 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 (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) { @@ -620,8 +626,8 @@ int APS5_VABI scePadGetVersionInfo(std::int32_t handle, PadVersionInfo* info) { int APS5_VABI scePadGetControllerType(std::int32_t handle, std::int32_t* type) { if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; if (type == nullptr) return PAD_ERROR_INVALID_ARG; - if (PortOf(handle) < 0) return PAD_ERROR_INVALID_HANDLE; - *type = PAD_CONTROLLER_TYPE_DUAL_SENSE; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; + *type = SpecialHandle(handle) ? PAD_CONTROLLER_TYPE_UNKNOWN : PAD_CONTROLLER_TYPE_DUAL_SENSE; return PAD_OK; } diff --git a/core/libs/prx/libScePad/include/Pad.hpp b/core/libs/prx/libScePad/include/Pad.hpp index a80582b500..49154aa264 100644 --- a/core/libs/prx/libScePad/include/Pad.hpp +++ b/core/libs/prx/libScePad/include/Pad.hpp @@ -29,6 +29,7 @@ constexpr int PAD_DEVICE_TYPE_DUAL_SHOCK_4 = 2; constexpr int PAD_DEVICE_TYPE_DUAL_SENSE = 3; constexpr int PAD_DEVICE_TYPE_DUAL_SENSE_EDGE = 4; +constexpr int PAD_CONTROLLER_TYPE_UNKNOWN = 0; constexpr int PAD_CONTROLLER_TYPE_DUAL_SENSE = 2; constexpr int PAD_CONNECT_TYPE_UNREGISTERED = 0; diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index 4e1453dbfe..c7eec141e9 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -15,6 +15,7 @@ namespace { struct Quat { float w = 1.0f, x = 0.0f, y = 0.0f, z = 0.0f; }; constexpr std::uint64_t kLatchExpiry = 100000; + constexpr std::uint64_t kLatchExpiryLimit = 500000; std::uint64_t injectedTimestamp = 0; struct QueuedInput { @@ -43,6 +44,8 @@ namespace { std::uint8_t pressedAnalogL2 = 0; std::uint8_t pressedAnalogR2 = 0; std::uint64_t pressedAt = 0; + std::uint64_t lastReadAt = 0; + std::uint64_t readInterval = 0; bool connected = false; }; @@ -140,6 +143,8 @@ void Pad::Initialize() { slots[i].lastFuseTime = now; slots[i].queue.clear(); slots[i].pressedSinceRead = 0; + slots[i].lastReadAt = 0; + slots[i].readInterval = 0; } } initialized = true; @@ -245,6 +250,10 @@ Pad::InputCommandFile::Input InjectedInput(std::uint64_t now) { } return injected; } + +std::uint64_t LatchExpiry(const PadSlot& slot) { + return std::clamp(slot.readInterval + slot.readInterval / 2, kLatchExpiry, kLatchExpiryLimit); +} } PadData Pad::ReadState(int port) { @@ -256,12 +265,14 @@ PadData Pad::ReadState(int port) { const auto injected = port == 0 ? InjectedInput(now) : Pad::InputCommandFile::Input{}; auto& slot = slots[port]; PadData data = BuildData(slot, slot.state, slot.timestamp, now, injected, port); - if (slot.pressedSinceRead != 0 && now - slot.pressedAt <= kLatchExpiry) { + if (slot.pressedSinceRead != 0 && now - slot.pressedAt <= LatchExpiry(slot)) { data.buttons |= slot.pressedSinceRead; if (slot.pressedSinceRead & 0x100u) data.analog_buttons_l2 = std::max(data.analog_buttons_l2, slot.pressedAnalogL2); if (slot.pressedSinceRead & 0x200u) data.analog_buttons_r2 = std::max(data.analog_buttons_r2, slot.pressedAnalogR2); } slot.pressedSinceRead = 0; + if (slot.lastReadAt != 0) slot.readInterval = now - slot.lastReadAt; + slot.lastReadAt = now; return data; } @@ -464,7 +475,7 @@ extern "C" void PadPublishInputForPort_nid_postfix(int port, const PadInputState const std::uint32_t pressed = input.buttons & ~slot.state.buttons; slot.timestamp = sceKernelGetProcessTime(); if (pressed != 0) { - if (slot.pressedSinceRead != 0 && slot.timestamp - slot.pressedAt > kLatchExpiry) slot.pressedSinceRead = 0; + if (slot.pressedSinceRead != 0 && slot.timestamp - slot.pressedAt > LatchExpiry(slot)) slot.pressedSinceRead = 0; slot.pressedSinceRead |= pressed; slot.pressedAt = slot.timestamp; if (pressed & 0x100u) slot.pressedAnalogL2 = input.analogButtonsL2; diff --git a/core/libs/prx/libScePlayGo/Export.cpp b/core/libs/prx/libScePlayGo/Export.cpp index eb53f29aec..f03bd2e343 100644 --- a/core/libs/prx/libScePlayGo/Export.cpp +++ b/core/libs/prx/libScePlayGo/Export.cpp @@ -107,13 +107,15 @@ static UnlistedChunkAnswer UnlistedChunks() { static bool IsKnownChunk(uint16_t id) { const auto& chunks = Chunks(); - if (chunks.defined) return chunks.ids.contains(id); - if (chunks.scenario || id == 0) return true; + if (chunks.ids.contains(id)) return true; + if (chunks.defined) return false; switch (UnlistedChunks()) { case UnlistedChunkAnswer::Installed: return true; case UnlistedChunkAnswer::Absent: return false; case UnlistedChunkAnswer::Stop: break; } + 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"); } diff --git a/core/libs/prx/libScePlayReady/Export.cpp b/core/libs/prx/libScePlayReady/Export.cpp index fa7958b257..4c7e5fe395 100644 --- a/core/libs/prx/libScePlayReady/Export.cpp +++ b/core/libs/prx/libScePlayReady/Export.cpp @@ -217,8 +217,7 @@ int APS5_VABI scePlayReadyMemFree() { } int APS5_VABI scePlayReadyMemGetStats() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80004001); } int APS5_VABI scePlayReadyMemRealloc() { diff --git a/core/libs/prx/libScePngDec/Export.cpp b/core/libs/prx/libScePngDec/Export.cpp index 3599cef8d3..c4404f2e40 100644 --- a/core/libs/prx/libScePngDec/Export.cpp +++ b/core/libs/prx/libScePngDec/Export.cpp @@ -145,10 +145,8 @@ int32_t APS5_VABI scePngDecDecode(void* handle, const PngDecDecodeParam* param, const std::optional header = Decoder::Png::ParseHeader(png); if (!header) return SCE_PNG_DEC_ERROR_INVALID_DATA; if (imageInfo) fillImageInfo(*header, imageInfo); - if (header->bitDepth == 16 && context->attribute == ATTRIBUTE_BIT_DEPTH_16) { - throw std::runtime_error("scePngDecDecode: 16-bit output is not implemented"); - } - + const bool packedDimensions = header->width <= MAX_PACKED_DIMENSION && header->height <= MAX_PACKED_DIMENSION; + if (!imageInfo && !packedDimensions) return SCE_PNG_DEC_ERROR_INVALID_ADDR; const std::uint64_t rowSize = static_cast(header->width) * BYTES_PER_PIXEL; const std::uint64_t pitch = param->image_pitch == 0 ? rowSize : param->image_pitch; if (param->image_pitch != 0 && (pitch < rowSize || (pitch & 3u) != 0)) return SCE_PNG_DEC_ERROR_INVALID_PARAM; @@ -160,6 +158,7 @@ int32_t APS5_VABI scePngDecDecode(void* handle, const PngDecDecodeParam* param, const std::uint64_t alignedWidth = (static_cast(header->width) + 7u) & ~std::uint64_t{7u}; const std::uint64_t alignedMaxWidth = (static_cast(context->maxImageWidth) + 7u) & ~std::uint64_t{7u}; if (alignedWidth > alignedMaxWidth) return SCE_PNG_DEC_ERROR_INVALID_WORK_MEMORY; + if (header->bitDepth > (8u << (context->attribute & 31u))) return SCE_PNG_DEC_ERROR_INVALID_WORK_MEMORY; const std::optional image = Decoder::Png::Decode(png); if (!image || image->width != header->width || image->height != header->height) return SCE_PNG_DEC_ERROR_DECODE_ERROR; @@ -179,7 +178,7 @@ int32_t APS5_VABI scePngDecDecode(void* handle, const PngDecDecodeParam* param, } } - if (header->width > MAX_PACKED_DIMENSION || header->height > MAX_PACKED_DIMENSION) return 0; + if (!packedDimensions) return 0; return static_cast(header->width << 16 | header->height); } diff --git a/core/libs/prx/libScePosixForWebKit/CMakeLists.txt b/core/libs/prx/libScePosixForWebKit/CMakeLists.txt index 036eb0577d..5b79f2a4f6 100644 --- a/core/libs/prx/libScePosixForWebKit/CMakeLists.txt +++ b/core/libs/prx/libScePosixForWebKit/CMakeLists.txt @@ -1,6 +1,6 @@ -add_library(libScePosixForWebKit SHARED EXCLUDE_FROM_ALL Export.cpp Resolver.cpp Filesystem.cpp) +add_library(libScePosixForWebKit SHARED EXCLUDE_FROM_ALL Export.cpp Resolver.cpp Filesystem.cpp Wrap.cpp) target_include_directories(libScePosixForWebKit PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(libScePosixForWebKit PRIVATE libSceNet) +target_link_libraries(libScePosixForWebKit PRIVATE libSceNet libkernel_sys libSceLibcInternal) if(WIN32) target_link_libraries(libScePosixForWebKit PRIVATE ws2_32) endif() @@ -17,6 +17,16 @@ if(BUILD_TESTING) set_tests_properties(guest_posix_filesystem PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_posix_wrap_tests ${LIBS_INCLUDE_DIR}/tests/GuestPosixWrap.cpp) + target_include_directories(guest_posix_wrap_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_posix_wrap_tests PRIVATE libScePosixForWebKit libkernel_sys libSceLibcInternal libkernel libc) + configure_windows_unwind(guest_posix_wrap_tests) + add_test(NAME guest_posix_wrap COMMAND guest_posix_wrap_tests) + set_tests_properties(guest_posix_wrap PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_posix_wrap PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_fnmatch_tests ${LIBS_INCLUDE_DIR}/tests/GuestFnmatch.cpp) target_include_directories(guest_fnmatch_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_fnmatch_tests PRIVATE libScePosixForWebKit libc) @@ -45,4 +55,14 @@ if(BUILD_TESTING) set_tests_properties(guest_service_database PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_posix_thread_name_tests ${LIBS_INCLUDE_DIR}/tests/GuestPosixThreadName.cpp) + target_include_directories(guest_posix_thread_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_posix_thread_name_tests PRIVATE libScePosixForWebKit libkernel libc) + configure_windows_unwind(guest_posix_thread_name_tests) + add_test(NAME guest_posix_thread_name COMMAND guest_posix_thread_name_tests) + set_tests_properties(guest_posix_thread_name PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_posix_thread_name PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libScePosixForWebKit/Export.cpp b/core/libs/prx/libScePosixForWebKit/Export.cpp index 6222bbb690..14675b52d3 100644 --- a/core/libs/prx/libScePosixForWebKit/Export.cpp +++ b/core/libs/prx/libScePosixForWebKit/Export.cpp @@ -6,14 +6,20 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/HostEntropy.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" #include #include #include #include +#include #include namespace { +constexpr int ThreadNameInvalid = 22; +constexpr int ThreadNameTooLong = 63; +constexpr std::size_t ThreadNameCapacity = 32; + constexpr int FnmNomatch = 1; constexpr int FnmNoescape = 0x01; constexpr int FnmPathname = 0x02; @@ -195,10 +201,6 @@ int APS5_VABI __wrap_clock_gettime(int clockId, KernelTimespec* tp) { return clock_gettime_nid_postfix(clockId, tp); } -int APS5_VABI __wrap_fchown() { - NotImplemented_nid_no_patch(__func__); - return 0; -} char* APS5_VABI __wrap_getcwd(char* buffer, std::size_t size) { return getcwd_nid_postfix(buffer, size); @@ -208,10 +210,6 @@ char* APS5_VABI __wrap_getenv(const char* name) { return getenv_nid_postfix(name); } -int APS5_VABI __wrap_link() { - NotImplemented_nid_no_patch(__func__); - return 0; -} int APS5_VABI __wrap_lstat(const char* path, FileStat* sb) { return lstat_nid_postfix(path, sb); @@ -221,20 +219,8 @@ void* APS5_VABI __wrap_mmap(void* address, std::size_t length, int protection, i return mmap_nid_postfix(address, length, protection, flags, descriptor, offset); } -int APS5_VABI __wrap_readlink() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI __wrap_statvfs() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI __wrap_symlink() { - NotImplemented_nid_no_patch(__func__); - return 0; -} int APS5_VABI clock_gettime_np() { NotImplemented_nid_no_patch(__func__); @@ -278,4 +264,12 @@ int APS5_VABI scePosixForWebKit_sysinfo() { return 0; } +int APS5_VABI pthread_setname_np_nid_postfix(Pthread thread, const char* name) { + if (thread == nullptr || name == nullptr) return ThreadNameInvalid; + if (std::strlen(name) >= ThreadNameCapacity) return ThreadNameTooLong; + std::lock_guard lock(thread->nameLock); + thread->name = name; + return 0; +} + } diff --git a/core/libs/prx/libScePosixForWebKit/Wrap.cpp b/core/libs/prx/libScePosixForWebKit/Wrap.cpp new file mode 100644 index 0000000000..f57bf16f66 --- /dev/null +++ b/core/libs/prx/libScePosixForWebKit/Wrap.cpp @@ -0,0 +1,27 @@ +#include "prx/libScePosixForWebKit/include/Api.hpp" +#include "prx/libkernel_sys/include/Api.hpp" +#include "prx/libSceLibcInternal/include/Api.hpp" + +extern "C" { + +int APS5_VABI __wrap_fchown(int descriptor, std::uint32_t owner, std::uint32_t group) { + return fchown_nid_postfix(descriptor, owner, group); +} + +int APS5_VABI __wrap_link(const char* existing, const char* created) { + return link_nid_postfix(existing, created); +} + +std::int64_t APS5_VABI __wrap_readlink(const char* path, char* buffer, std::size_t size) { + return readlink_nid_postfix(path, buffer, size); +} + +int APS5_VABI __wrap_statvfs(const char* path, LibcStatvfs* status) { + return statvfs_nid_postfix(path, status); +} + +int APS5_VABI __wrap_symlink(const char* target, const char* created) { + return symlink_nid_postfix(target, created); +} + +} diff --git a/core/libs/prx/libScePosixForWebKit/include/Api.hpp b/core/libs/prx/libScePosixForWebKit/include/Api.hpp index fdd68aa8f7..96834f0ad7 100644 --- a/core/libs/prx/libScePosixForWebKit/include/Api.hpp +++ b/core/libs/prx/libScePosixForWebKit/include/Api.hpp @@ -1,6 +1,7 @@ #pragma once #include "prx/libScePosixForWebKit/GuestResolver.hpp" +#include "prx/libSceLibcInternal/include/Statvfs.hpp" #include "SceTypes.hpp" extern "C" { @@ -11,18 +12,19 @@ void APS5_VABI arc4random_buf_nid_postfix(void* buffer, std::size_t size); std::uint32_t APS5_VABI arc4random_nid_postfix(); int APS5_VABI __wrap_access(const char* path, int mode); int APS5_VABI __wrap_clock_gettime(int clockId, KernelTimespec* tp); -int APS5_VABI __wrap_fchown(); +int APS5_VABI __wrap_fchown(int descriptor, std::uint32_t owner, std::uint32_t group); char* APS5_VABI __wrap_getcwd(char* buffer, std::size_t size); char* APS5_VABI __wrap_getenv(const char* name); -int APS5_VABI __wrap_link(); +int APS5_VABI __wrap_link(const char* existing, const char* created); int APS5_VABI __wrap_lstat(const char* path, FileStat* sb); void* APS5_VABI __wrap_mmap(void* address, std::size_t length, int protection, int flags, int descriptor, std::int64_t offset); -int APS5_VABI __wrap_readlink(); -int APS5_VABI __wrap_statvfs(); -int APS5_VABI __wrap_symlink(); +std::int64_t APS5_VABI __wrap_readlink(const char* path, char* buffer, std::size_t size); +int APS5_VABI __wrap_statvfs(const char* path, LibcStatvfs* status); +int APS5_VABI __wrap_symlink(const char* target, const char* created); int APS5_VABI clock_gettime_np(); int APS5_VABI getenv_np(); int APS5_VABI scePosixForWebKit_gethostbyname_r(); +int APS5_VABI pthread_setname_np_nid_postfix(Pthread thread, const char* name); int APS5_VABI mmap_np(); int APS5_VABI msetname(); int APS5_VABI pthread_setschedparam_np(); diff --git a/core/libs/prx/libScePsml_debug/Export.cpp b/core/libs/prx/libScePsml_debug/Export.cpp index 3de9fc4bf6..c93e6d9ca8 100644 --- a/core/libs/prx/libScePsml_debug/Export.cpp +++ b/core/libs/prx/libScePsml_debug/Export.cpp @@ -1,4 +1,5 @@ #include "prx/libScePsml_debug/include/Api.hpp" +#include "prx/libkernel/libkernel.hpp" #include #include #include @@ -17,6 +18,11 @@ namespace { constexpr std::int32_t SCE_PSML_ERROR_NOT_INITIALIZED = static_cast(0x8A810001); constexpr std::int32_t SCE_PSML_ERROR_INVALID_ARGUMENT = static_cast(0x8A810005); constexpr std::int32_t SCE_PSML_ERROR_NULL_OUTPUT = static_cast(0x8A810009); +constexpr std::int32_t SCE_PSML_ERROR_UNSUPPORTED_TYPE = static_cast(0x8A81000D); +constexpr std::int32_t SCE_PSML_ERROR_UNSUPPORTED_CONSOLE = static_cast(0x8A810012); +constexpr std::int32_t SCE_PSML_ERROR_SHARED_RESOURCES_BUSY = static_cast(0x8A810015); +constexpr std::int32_t Mfsr2ErrorNotInitialized = static_cast(0x8A8A0001); +constexpr std::int32_t Mfsr2ErrorUnsupportedConsole = static_cast(0x8A8A0012); constexpr std::uint64_t MfsrBlockBytes = 0x200000; constexpr std::uint64_t MfsrBlockCount = 4; constexpr std::uint32_t MfsrDispatchPacketDwords = 0x600; @@ -148,24 +154,31 @@ std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1100(void* context, void* } std::int32_t APS5_VABI scePsmlMfsrInit() { - g_mfsrInitialized = true; + if (sceKernelIsTrinityMode() == 0) return SCE_PSML_ERROR_UNSUPPORTED_CONSOLE; + NotImplemented_nid_no_patch(__func__); return 0; } std::int32_t APS5_VABI scePsmlMfsrGetSharedResourcesInitRequirement(MfsrRequirement* requirement, const MfsrSharedResourcesParam* param) { if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; RequireMfsr(requirement != nullptr && param != nullptr, __func__, "null requirement or parameter"); - RequireMfsr(param->type == 0, __func__, ("shared resources type " + std::to_string(param->type) + " is not supported").c_str()); + if (param->type != 0) return SCE_PSML_ERROR_UNSUPPORTED_TYPE; *requirement = {MfsrBlockBytes, MfsrBlockBytes, MfsrSharedBlockCount}; return 0; } std::int32_t APS5_VABI scePsmlMfsrCreateSharedResources(void** sharedResources, const MfsrSharedResourcesParam* param) { if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + std::lock_guard lock(g_mfsrMutex); + for (const auto& [handle, object] : g_mfsrObjects) { + if (object->kind == MfsrObjectKind::SharedResources) return SCE_PSML_ERROR_SHARED_RESOURCES_BUSY; + } RequireMfsr(sharedResources != nullptr && param != nullptr, __func__, "null handle or parameter"); - RequireMfsr(param->type == 0, __func__, ("shared resources type " + std::to_string(param->type) + " is not supported").c_str()); + if (param->type != 0) return SCE_PSML_ERROR_UNSUPPORTED_TYPE; RequireBlocks(param->blocks, param->blockCount, MfsrSharedBlockCount, __func__); - *sharedResources = CreateMfsrObject(MfsrObjectKind::SharedResources, nullptr, __func__); + auto* object = new MfsrObject{MfsrObjectKind::SharedResources, nullptr, 0, 0}; + g_mfsrObjects.emplace(object, object); + *sharedResources = object; return 0; } @@ -268,49 +281,57 @@ std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket900(void* context, void* return SCE_PSML_ERROR_NOT_INITIALIZED; } -int APS5_VABI scePsmlMfsr2ReleaseSharedResources(void* sharedResources) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsr2ReleaseSharedResources(void* sharedResources) { + (void)sharedResources; + return Mfsr2ErrorNotInitialized; } -int APS5_VABI scePsmlMfsr2GetSharedResourcesInitRequirement(MfsrRequirement* requirement, const Mfsr2SharedResourcesParam* param) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsr2GetSharedResourcesInitRequirement(MfsrRequirement* requirement, const Mfsr2SharedResourcesParam* param) { + (void)requirement; + (void)param; + return Mfsr2ErrorNotInitialized; } -int APS5_VABI scePsmlMfsr2GetContextInitRequirement(MfsrRequirement* requirement, const Mfsr2ContextParam* param) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsr2GetContextInitRequirement(MfsrRequirement* requirement, const Mfsr2ContextParam* param) { + (void)requirement; + (void)param; + return Mfsr2ErrorNotInitialized; } -int APS5_VABI scePsmlMfsr2ReleaseContext(void* context) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsr2ReleaseContext(void* context) { + (void)context; + return Mfsr2ErrorNotInitialized; } -int APS5_VABI scePsmlMfsr2GetDispatchPacketsSizeInDwords() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsr2GetDispatchPacketsSizeInDwords(void* context, std::uint32_t* packetSizeInDwords) { + (void)context; + (void)packetSizeInDwords; + return Mfsr2ErrorNotInitialized; } -int APS5_VABI scePsmlMfsr2CreateSharedResources(void** sharedResources, const Mfsr2SharedResourcesParam* param) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsr2CreateSharedResources(void** sharedResources, const Mfsr2SharedResourcesParam* param) { + (void)sharedResources; + (void)param; + return Mfsr2ErrorNotInitialized; } -int APS5_VABI scePsmlMfsr2GetDispatchPackets(void* context, const MfsrCommandBuffer* commandBuffer, const Mfsr2DispatchParam* param) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsr2GetDispatchPackets(void* context, const MfsrCommandBuffer* commandBuffer, const Mfsr2DispatchParam* param) { + (void)context; + (void)commandBuffer; + (void)param; + return Mfsr2ErrorNotInitialized; } -int APS5_VABI scePsmlMfsr2CreateContext(void** context, const Mfsr2ContextParam* param) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsr2CreateContext(void** context, const Mfsr2ContextParam* param) { + (void)context; + (void)param; + return Mfsr2ErrorNotInitialized; } -int APS5_VABI scePsmlMfsr2Init() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsr2Init() { + if (sceKernelIsTrinityMode() == 0) return Mfsr2ErrorUnsupportedConsole; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI scePsmlMfsrEnableSpecularAA() { diff --git a/core/libs/prx/libScePsml_debug/include/Api.hpp b/core/libs/prx/libScePsml_debug/include/Api.hpp index 84c5817117..4a79a346d5 100644 --- a/core/libs/prx/libScePsml_debug/include/Api.hpp +++ b/core/libs/prx/libScePsml_debug/include/Api.hpp @@ -118,15 +118,15 @@ std::int32_t APS5_VABI scePsmlMfsrRequestCapture(void* context); std::int32_t APS5_VABI scePsmlMfsrIsCaptureInProgress(void* context); std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1000(void* context, void* commandBuffer, const void* param); std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket900(void* context, void* commandBuffer, const void* param); -int APS5_VABI scePsmlMfsr2ReleaseSharedResources(void* sharedResources); -int APS5_VABI scePsmlMfsr2GetSharedResourcesInitRequirement(MfsrRequirement* requirement, const Mfsr2SharedResourcesParam* param); -int APS5_VABI scePsmlMfsr2GetContextInitRequirement(MfsrRequirement* requirement, const Mfsr2ContextParam* param); -int APS5_VABI scePsmlMfsr2ReleaseContext(void* context); -int APS5_VABI scePsmlMfsr2GetDispatchPacketsSizeInDwords(); -int APS5_VABI scePsmlMfsr2CreateSharedResources(void** sharedResources, const Mfsr2SharedResourcesParam* param); -int APS5_VABI scePsmlMfsr2GetDispatchPackets(void* context, const MfsrCommandBuffer* commandBuffer, const Mfsr2DispatchParam* param); -int APS5_VABI scePsmlMfsr2CreateContext(void** context, const Mfsr2ContextParam* param); -int APS5_VABI scePsmlMfsr2Init(); +std::int32_t APS5_VABI scePsmlMfsr2ReleaseSharedResources(void* sharedResources); +std::int32_t APS5_VABI scePsmlMfsr2GetSharedResourcesInitRequirement(MfsrRequirement* requirement, const Mfsr2SharedResourcesParam* param); +std::int32_t APS5_VABI scePsmlMfsr2GetContextInitRequirement(MfsrRequirement* requirement, const Mfsr2ContextParam* param); +std::int32_t APS5_VABI scePsmlMfsr2ReleaseContext(void* context); +std::int32_t APS5_VABI scePsmlMfsr2GetDispatchPacketsSizeInDwords(void* context, std::uint32_t* packetSizeInDwords); +std::int32_t APS5_VABI scePsmlMfsr2CreateSharedResources(void** sharedResources, const Mfsr2SharedResourcesParam* param); +std::int32_t APS5_VABI scePsmlMfsr2GetDispatchPackets(void* context, const MfsrCommandBuffer* commandBuffer, const Mfsr2DispatchParam* param); +std::int32_t APS5_VABI scePsmlMfsr2CreateContext(void** context, const Mfsr2ContextParam* param); +std::int32_t APS5_VABI scePsmlMfsr2Init(); int APS5_VABI scePsmlMfsrEnableSpecularAA(); int APS5_VABI scePsmlMfsrEnableQuality8K(); diff --git a/core/libs/prx/libSceRegMgr/Export.cpp b/core/libs/prx/libSceRegMgr/Export.cpp index d1efe47018..84326f6c25 100644 --- a/core/libs/prx/libSceRegMgr/Export.cpp +++ b/core/libs/prx/libSceRegMgr/Export.cpp @@ -1,22 +1,93 @@ #include "prx/libSceRegMgr/include/Api.hpp" #include #include +#include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int SCE_REG_MGR_ERROR_INVALID_PARAM = static_cast(0x80820001); +constexpr int SCE_REG_MGR_ERROR_NOT_FOUND = static_cast(0x80820002); +constexpr int SCE_REG_MGR_ERROR_BUFFER_TOO_SMALL = static_cast(0x80820004); +constexpr int SCE_REG_MGR_ERROR_INVALID_TYPE = static_cast(0x80820006); + +enum EntryType { + TypeInt = 0, + TypeStr = 1, + TypeBin = 2, +}; + +struct RegEntry { + EntryType type; + int32_t intVal = 0; + std::string strVal; + std::vector binVal; +}; + +class RegistryStore { +public: + RegistryStore() = default; + + int getInt(int64_t key, int32_t* out) { + if (out == nullptr) return SCE_REG_MGR_ERROR_INVALID_PARAM; + std::lock_guard lock(mutex_); + auto it = entries_.find(key); + if (it == entries_.end()) return SCE_REG_MGR_ERROR_NOT_FOUND; + if (it->second.type != TypeInt) return SCE_REG_MGR_ERROR_INVALID_TYPE; + *out = it->second.intVal; + return 0; + } + + int getStr(int64_t key, char* out, int len) { + if (len == 0) NotImplemented_nid_no_patch(__func__); + if (out == nullptr || len < 0) return SCE_REG_MGR_ERROR_INVALID_PARAM; + std::lock_guard lock(mutex_); + auto it = entries_.find(key); + if (it == entries_.end()) return SCE_REG_MGR_ERROR_NOT_FOUND; + if (it->second.type != TypeStr) return SCE_REG_MGR_ERROR_INVALID_TYPE; + if (static_cast(len) <= it->second.strVal.size()) return SCE_REG_MGR_ERROR_BUFFER_TOO_SMALL; + std::memcpy(out, it->second.strVal.c_str(), it->second.strVal.size() + 1); + return 0; + } + + int getBin(int64_t key, void* out, int size) { + if (size == 0) NotImplemented_nid_no_patch(__func__); + if (out == nullptr || size < 0) return SCE_REG_MGR_ERROR_INVALID_PARAM; + std::lock_guard lock(mutex_); + auto it = entries_.find(key); + if (it == entries_.end()) return SCE_REG_MGR_ERROR_NOT_FOUND; + if (it->second.type != TypeBin) return SCE_REG_MGR_ERROR_INVALID_TYPE; + if (static_cast(size) < it->second.binVal.size()) return SCE_REG_MGR_ERROR_BUFFER_TOO_SMALL; + std::memcpy(out, it->second.binVal.data(), it->second.binVal.size()); + return 0; + } + +private: + std::mutex mutex_; + std::unordered_map entries_; +}; + +RegistryStore gStore; + +} // namespace + extern "C" { -int APS5_VABI sceRegMgrGetBin(std::uint32_t optionId, void* data, std::size_t size) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceRegMgrGetBin(std::int64_t optionId, void* data, int size) { + return gStore.getBin(optionId, data, size); } -int APS5_VABI sceRegMgrGetInt(int32_t optionId, int32_t* outInt) { - NotImplemented_nid_no_patch(__func__); - return 0; + +int APS5_VABI sceRegMgrGetInt(std::int64_t optionId, int32_t* outInt) { + return gStore.getInt(optionId, outInt); } -int APS5_VABI sceRegMgrGetStr(int32_t optionId, char* outString, std::size_t stringLength) { - NotImplemented_nid_no_patch(__func__); - return 0; + +int APS5_VABI sceRegMgrGetStr(std::int64_t optionId, char* outString, int stringLength) { + return gStore.getStr(optionId, outString, stringLength); } int APS5_VABI sceRegMgrSetBin(std::uint32_t optionId, const void* data, std::size_t size) { NotImplemented_nid_no_patch(__func__); diff --git a/core/libs/prx/libSceRegMgr/include/Api.hpp b/core/libs/prx/libSceRegMgr/include/Api.hpp index 91e57f01f1..8d062d5377 100644 --- a/core/libs/prx/libSceRegMgr/include/Api.hpp +++ b/core/libs/prx/libSceRegMgr/include/Api.hpp @@ -1,12 +1,14 @@ #pragma once +#include +#include #include "SceTypes.hpp" extern "C" { -int APS5_VABI sceRegMgrGetBin(std::uint32_t optionId, void* data, std::size_t size); -int APS5_VABI sceRegMgrGetInt(int32_t optionId, int32_t* outInt); -int APS5_VABI sceRegMgrGetStr(int32_t optionId, char* outString, std::size_t stringLength); +int APS5_VABI sceRegMgrGetBin(std::int64_t optionId, void* data, int size); +int APS5_VABI sceRegMgrGetInt(std::int64_t optionId, int32_t* outInt); +int APS5_VABI sceRegMgrGetStr(std::int64_t optionId, char* outString, int stringLength); int APS5_VABI sceRegMgrSetBin(std::uint32_t optionId, const void* data, std::size_t size); int APS5_VABI sceRegMgrSetInt(int32_t optionId, int32_t inInt); int APS5_VABI sceRegMgrSetStr(int32_t optionId, const char* inStirng, std::size_t stringLength); diff --git a/core/libs/prx/libSceSaveData.native/CMakeLists.txt b/core/libs/prx/libSceSaveData.native/CMakeLists.txt index 6b03a2c86c..5236fba466 100644 --- a/core/libs/prx/libSceSaveData.native/CMakeLists.txt +++ b/core/libs/prx/libSceSaveData.native/CMakeLists.txt @@ -16,6 +16,7 @@ if(BUILD_TESTING) add_test_executable(guest_savedata_delete_tests ${LIBS_INCLUDE_DIR}/tests/GuestSaveDataDelete.cpp) target_include_directories(guest_savedata_delete_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_savedata_delete_tests PRIVATE libSceSaveData.native libc) + configure_windows_unwind(guest_savedata_delete_tests) add_test(NAME guest_savedata_delete COMMAND guest_savedata_delete_tests) if(WIN32) set_tests_properties(guest_savedata_delete PROPERTIES ENVIRONMENT_MODIFICATION @@ -25,6 +26,7 @@ if(BUILD_TESTING) add_test_executable(guest_savedata_convert_tests ${LIBS_INCLUDE_DIR}/tests/GuestSaveDataConvert.cpp) target_include_directories(guest_savedata_convert_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_savedata_convert_tests PRIVATE libSceSaveData.native libc) + configure_windows_unwind(guest_savedata_convert_tests) add_test(NAME guest_savedata_convert COMMAND guest_savedata_convert_tests) if(WIN32) set_tests_properties(guest_savedata_convert PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 916a9dbe26..0627767b30 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -330,7 +330,7 @@ static bool isMountedDirectory(const std::string& path) { static int deleteSave(const SaveDataDelete* del) { if (del == nullptr || del->dir_name == nullptr) { - throw std::runtime_error("sceSaveDataDelete: null argument"); + 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) { @@ -405,7 +405,7 @@ static SaveDataSearchInfo search_info(const std::string& real_path) { static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameSearchResult* result) { if (cond == nullptr || result == nullptr) { - throw std::runtime_error("sceSaveDataDirNameSearch: null argument"); + return SAVE_DATA_ERROR_PARAMETER; } result->hit_num = 0; result->set_num = 0; @@ -608,7 +608,7 @@ int APS5_VABI sceSaveDataLoadIcon(const SaveDataMountPoint* mount_point, SaveDat static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result) { if (mount == nullptr || mount_result == nullptr || mount->dir_name == nullptr) { - throw std::runtime_error("sceSaveDataMount3: null argument"); + return SAVE_DATA_ERROR_PARAMETER; } const bool create = (mount->mount_mode & SAVE_DATA_MOUNT_MODE_CREATE) != 0; const bool create2 = (mount->mount_mode & SAVE_DATA_MOUNT_MODE_CREATE2) != 0; diff --git a/core/libs/prx/libSceSdma/Export.cpp b/core/libs/prx/libSceSdma/Export.cpp index 1e67f3a5bb..45a5fcc447 100644 --- a/core/libs/prx/libSceSdma/Export.cpp +++ b/core/libs/prx/libSceSdma/Export.cpp @@ -93,7 +93,6 @@ int APS5_VABI sceSdmaFenceNonBlocking(void) { } int APS5_VABI sceSdmaFinalize(void) { - NotImplemented_nid_no_patch(__func__); return 0; } @@ -103,12 +102,10 @@ int APS5_VABI sceSdmaFlushAll(void) { } int APS5_VABI sceSdmaFlushAllWithEvent(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x809900c9); } int APS5_VABI sceSdmaInitialize(void) { - NotImplemented_nid_no_patch(__func__); return 0; } @@ -123,8 +120,7 @@ int APS5_VABI sceSdmaOpen(void) { } int APS5_VABI sceSdmaQueryWorkingBufferSize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 0x8000; } int APS5_VABI sceSdmaTryFlush(void) { @@ -133,8 +129,7 @@ int APS5_VABI sceSdmaTryFlush(void) { } int APS5_VABI sceSdmaWaitForEvent(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x809900c9); } } diff --git a/core/libs/prx/libSceSharePlay/Export.cpp b/core/libs/prx/libSceSharePlay/Export.cpp index 05910a7c56..a19d94eb98 100644 --- a/core/libs/prx/libSceSharePlay/Export.cpp +++ b/core/libs/prx/libSceSharePlay/Export.cpp @@ -51,8 +51,13 @@ int APS5_VABI sceSharePlayCrashDaemon(void) { return 0; } -int APS5_VABI sceSharePlayGetCurrentConnectionInfo(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSharePlayGetCurrentConnectionInfo(SharePlayConnectionInfo* info) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED; + if (info == nullptr) return SCE_SHARE_PLAY_ERROR_INVALID_ARGS; + *info = {}; + info->hostUserId = SCE_USER_SERVICE_USER_ID_INVALID; + info->visitorUserId = SCE_USER_SERVICE_USER_ID_INVALID; return 0; } diff --git a/core/libs/prx/libSceSharePlay/include/Api.hpp b/core/libs/prx/libSceSharePlay/include/Api.hpp index 6336eae897..317c0dc43c 100644 --- a/core/libs/prx/libSceSharePlay/include/Api.hpp +++ b/core/libs/prx/libSceSharePlay/include/Api.hpp @@ -9,7 +9,7 @@ int APS5_VABI sceSharePlayTerminate(void); int APS5_VABI sceSharePlayGetCurrentConnectionInfoA(SharePlayConnectionInfoA* info); int APS5_VABI sceSharePlayProhibitController(std::uint8_t prohibited); int APS5_VABI sceSharePlayCrashDaemon(void); -int APS5_VABI sceSharePlayGetCurrentConnectionInfo(void); +int APS5_VABI sceSharePlayGetCurrentConnectionInfo(SharePlayConnectionInfo* info); int APS5_VABI sceSharePlayGetCurrentInfo(void); int APS5_VABI sceSharePlayGetEvent(void); int APS5_VABI sceSharePlayNotifyDialogOpen(void); diff --git a/core/libs/prx/libSceSpSysCallWrapper/CMakeLists.txt b/core/libs/prx/libSceSpSysCallWrapper/CMakeLists.txt new file mode 100644 index 0000000000..5e407e9a37 --- /dev/null +++ b/core/libs/prx/libSceSpSysCallWrapper/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceSpSysCallWrapper SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceSpSysCallWrapper PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceSpSysCallWrapper PRIVATE libSceNet) +configure_windows_unwind(libSceSpSysCallWrapper) + +if(BUILD_TESTING) + add_test_executable(guest_sp_syscall_wrapper_tests ${LIBS_INCLUDE_DIR}/tests/GuestSpSysCallWrapper.cpp) + target_include_directories(guest_sp_syscall_wrapper_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sp_syscall_wrapper_tests PRIVATE libSceSpSysCallWrapper libSceNet libkernel libc) + configure_windows_unwind(guest_sp_syscall_wrapper_tests) + add_test(NAME guest_sp_syscall_wrapper COMMAND guest_sp_syscall_wrapper_tests) + if(WIN32) + set_tests_properties(guest_sp_syscall_wrapper PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceSpSysCallWrapper/Export.cpp b/core/libs/prx/libSceSpSysCallWrapper/Export.cpp index 485d2fb98f..12371b71f2 100644 --- a/core/libs/prx/libSceSpSysCallWrapper/Export.cpp +++ b/core/libs/prx/libSceSpSysCallWrapper/Export.cpp @@ -1,4 +1,6 @@ -#include "prx/libSceSpSysCallWrapper/include/Api.hpp" +#include "prx/libSceSpSysCallWrapper/libSceSpSysCallWrapper.hpp" +#include "prx/libSceNet/libSceNet.hpp" +#include "prx/libkernel/libkernel.hpp" #include #include #include "SceTypes.hpp" @@ -6,244 +8,199 @@ extern "C" { +std::int64_t APS5_VABI GuestNetRecv_nid_no_patch(int s, void* buf, std::size_t len, int flags); +std::int64_t APS5_VABI GuestNetSend_nid_no_patch(int s, const void* buf, std::size_t len, int flags); + int APS5_VABI sceSpGetPid() { - NotImplemented_nid_no_patch(__func__); - return 0; + return getpid_nid_postfix(); } -int APS5_VABI sceSpKernelGettimeofday() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpKernelGettimeofday(KernelTimeval* tp) { + return sceKernelGettimeofday(tp); } -int APS5_VABI sceSpKernelNanosleep() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpKernelNanosleep(const KernelTimespec* rqtp, KernelTimespec* rmtp) { + return sceKernelNanosleep(rqtp, rmtp); } -int APS5_VABI sceSpNetConnect() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetConnect(int s, const void* addr, std::uint32_t addrlen) { + return sceNetConnect(s, addr, addrlen); } -int APS5_VABI sceSpNetEpollAbort() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetEpollAbort(int eid, int flags) { + return sceNetEpollAbort(eid, flags); } -int APS5_VABI sceSpNetEpollControl() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetEpollControl(int eid, int op, int id, const void* event) { + return sceNetEpollControl(eid, op, id, static_cast(event)); } -int APS5_VABI sceSpNetEpollCreate() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetEpollCreate(const char* name, int flags) { + return sceNetEpollCreate(name, flags); } -int APS5_VABI sceSpNetEpollDestroy() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetEpollDestroy(int eid) { + return sceNetEpollDestroy(eid); } -int APS5_VABI sceSpNetEpollWait() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetEpollWait(int eid, void* events, int maxevents, int timeout) { + return sceNetEpollWait(eid, static_cast(events), maxevents, timeout); } -int APS5_VABI sceSpNetErrnoLoc() { - NotImplemented_nid_no_patch(__func__); - return 0; +int* APS5_VABI sceSpNetErrnoLoc() { + return sceNetErrnoLoc(); } -int APS5_VABI sceSpNetPoolCreate() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetPoolCreate(const char* name, int size, int flags) { + return sceNetPoolCreate(name, size, flags); } -int APS5_VABI sceSpNetPoolDestroy() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetPoolDestroy(int memid) { + return sceNetPoolDestroy(memid); } -int APS5_VABI sceSpNetRecv() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceSpNetRecv(int s, void* buf, std::size_t len, int flags) { + return GuestNetRecv_nid_no_patch(s, buf, len, flags); } -int APS5_VABI sceSpNetResolverAbort() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetResolverAbort(int rid, int flags) { + return sceNetResolverAbort(rid, flags); } -int APS5_VABI sceSpNetResolverCreate() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetResolverCreate(const char* name, int memid, int flags) { + return sceNetResolverCreate(name, memid, flags); } -int APS5_VABI sceSpNetResolverDestroy() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetResolverDestroy(int rid) { + return sceNetResolverDestroy(rid); } -int APS5_VABI sceSpNetResolverGetError() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetResolverGetError(int rid, int* status) { + return sceNetResolverGetError(rid, status); } -int APS5_VABI sceSpNetResolverStartNtoa() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout, int retry, int flags) { + return sceNetResolverStartNtoa(rid, hostname, addr, timeout, retry, flags); } -int APS5_VABI sceSpNetSend() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceSpNetSend(int s, const void* buf, std::size_t len, int flags) { + return GuestNetSend_nid_no_patch(s, buf, len, flags); } -int APS5_VABI sceSpNetSetsockopt() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetSetsockopt(int s, int level, int optname, const void* optval, std::uint32_t optlen) { + return sceNetSetsockopt(s, level, optname, optval, optlen); } -int APS5_VABI sceSpNetSocket() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetSocket(const char* name, int family, int type, int protocol) { + return sceNetSocket(name, family, type, protocol); } -int APS5_VABI sceSpNetSocketClose() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetSocketClose(int s) { + return sceNetSocketClose(s); } -int APS5_VABI sceSpPthreadAttrDestroy() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadAttrDestroy(PthreadAttr* attr) { + return scePthreadAttrDestroy(attr); } -int APS5_VABI sceSpPthreadAttrInit() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadAttrInit(PthreadAttr* attr) { + return scePthreadAttrInit(attr); } -int APS5_VABI sceSpPthreadAttrSetinheritsched() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched) { + return scePthreadAttrSetinheritsched(attr, inheritSched); } -int APS5_VABI sceSpPthreadAttrSetschedparam() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param) { + return scePthreadAttrSetschedparam(attr, param); } -int APS5_VABI sceSpPthreadAttrSetstacksize() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSize) { + return scePthreadAttrSetstacksize(attr, stackSize); } -int APS5_VABI sceSpPthreadCondDestroy() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadCondDestroy(PthreadCond* cond) { + return scePthreadCondDestroy(cond); } -int APS5_VABI sceSpPthreadCondInit() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char* name) { + return scePthreadCondInit(cond, attr, name); } -int APS5_VABI sceSpPthreadCondSignal() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadCondSignal(PthreadCond* cond) { + return scePthreadCondSignal(cond); } -int APS5_VABI sceSpPthreadCondWait() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadCondWait(PthreadCond* cond, PthreadMutex* mutex) { + return scePthreadCondWait(cond, mutex); } -int APS5_VABI sceSpPthreadCondattrDestroy() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadCondattrDestroy(PthreadCondattr* attr) { + return scePthreadCondattrDestroy(attr); } -int APS5_VABI sceSpPthreadCondattrInit() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadCondattrInit(PthreadCondattr* attr) { + return scePthreadCondattrInit(attr); } -int APS5_VABI sceSpPthreadCreate() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name) { + return scePthreadCreate(thread, attr, entry, arg, name); } -int APS5_VABI sceSpPthreadDetach() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadDetach(Pthread thread) { + return scePthreadDetach(thread); } -int APS5_VABI sceSpPthreadExit() { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI sceSpPthreadExit(void* retval) { + scePthreadExit(retval); } -int APS5_VABI sceSpPthreadJoin() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadJoin(Pthread thread, void** retval) { + return scePthreadJoin(thread, retval); } -int APS5_VABI sceSpPthreadMutexDestroy() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadMutexDestroy(PthreadMutex* mutex) { + return scePthreadMutexDestroy(mutex); } -int APS5_VABI sceSpPthreadMutexInit() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name) { + return scePthreadMutexInit(mutex, attr, name); } -int APS5_VABI sceSpPthreadMutexLock() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadMutexLock(PthreadMutex* mutex) { + return scePthreadMutexLock(mutex); } -int APS5_VABI sceSpPthreadMutexUnlock() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadMutexUnlock(PthreadMutex* mutex) { + return scePthreadMutexUnlock(mutex); } -int APS5_VABI sceSpPthreadMutexattrDestroy() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadMutexattrDestroy(PthreadMutexattr* attr) { + return scePthreadMutexattrDestroy(attr); } -int APS5_VABI sceSpPthreadMutexattrInit() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadMutexattrInit(PthreadMutexattr* attr) { + return scePthreadMutexattrInit(attr); } -int APS5_VABI sceSpPthreadMutexattrSetprioceiling() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadMutexattrSetprioceiling(PthreadMutexattr* attr, int prioceiling) { + return scePthreadMutexattrSetprioceiling(attr, prioceiling); } -int APS5_VABI sceSpPthreadMutexattrSetprotocol() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadMutexattrSetprotocol(PthreadMutexattr* attr, int protocol) { + return scePthreadMutexattrSetprotocol(attr, protocol); } -int APS5_VABI sceSpPthreadMutexattrSettype() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadMutexattrSettype(PthreadMutexattr* attr, int type) { + return scePthreadMutexattrSettype(attr, type); } -int APS5_VABI sceSpPthreadSetprio() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadSetprio(Pthread thread, int prio) { + return scePthreadSetprio(thread, prio); } -int APS5_VABI sceSpSelect() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpSelect(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout) { + return select_nid_postfix(nfds, readfds, writefds, exceptfds, timeout); } } diff --git a/core/libs/prx/libSceSpSysCallWrapper/include/Api.hpp b/core/libs/prx/libSceSpSysCallWrapper/include/Api.hpp index 86136aef04..2ef4e11459 100644 --- a/core/libs/prx/libSceSpSysCallWrapper/include/Api.hpp +++ b/core/libs/prx/libSceSpSysCallWrapper/include/Api.hpp @@ -1,56 +1,58 @@ #pragma once +#include +#include #include "SceTypes.hpp" extern "C" { int APS5_VABI sceSpGetPid(); -int APS5_VABI sceSpKernelGettimeofday(); -int APS5_VABI sceSpKernelNanosleep(); -int APS5_VABI sceSpNetConnect(); -int APS5_VABI sceSpNetEpollAbort(); -int APS5_VABI sceSpNetEpollControl(); -int APS5_VABI sceSpNetEpollCreate(); -int APS5_VABI sceSpNetEpollDestroy(); -int APS5_VABI sceSpNetEpollWait(); -int APS5_VABI sceSpNetErrnoLoc(); -int APS5_VABI sceSpNetPoolCreate(); -int APS5_VABI sceSpNetPoolDestroy(); -int APS5_VABI sceSpNetRecv(); -int APS5_VABI sceSpNetResolverAbort(); -int APS5_VABI sceSpNetResolverCreate(); -int APS5_VABI sceSpNetResolverDestroy(); -int APS5_VABI sceSpNetResolverGetError(); -int APS5_VABI sceSpNetResolverStartNtoa(); -int APS5_VABI sceSpNetSend(); -int APS5_VABI sceSpNetSetsockopt(); -int APS5_VABI sceSpNetSocket(); -int APS5_VABI sceSpNetSocketClose(); -int APS5_VABI sceSpPthreadAttrDestroy(); -int APS5_VABI sceSpPthreadAttrInit(); -int APS5_VABI sceSpPthreadAttrSetinheritsched(); -int APS5_VABI sceSpPthreadAttrSetschedparam(); -int APS5_VABI sceSpPthreadAttrSetstacksize(); -int APS5_VABI sceSpPthreadCondDestroy(); -int APS5_VABI sceSpPthreadCondInit(); -int APS5_VABI sceSpPthreadCondSignal(); -int APS5_VABI sceSpPthreadCondWait(); -int APS5_VABI sceSpPthreadCondattrDestroy(); -int APS5_VABI sceSpPthreadCondattrInit(); -int APS5_VABI sceSpPthreadCreate(); -int APS5_VABI sceSpPthreadDetach(); -int APS5_VABI sceSpPthreadExit(); -int APS5_VABI sceSpPthreadJoin(); -int APS5_VABI sceSpPthreadMutexDestroy(); -int APS5_VABI sceSpPthreadMutexInit(); -int APS5_VABI sceSpPthreadMutexLock(); -int APS5_VABI sceSpPthreadMutexUnlock(); -int APS5_VABI sceSpPthreadMutexattrDestroy(); -int APS5_VABI sceSpPthreadMutexattrInit(); -int APS5_VABI sceSpPthreadMutexattrSetprioceiling(); -int APS5_VABI sceSpPthreadMutexattrSetprotocol(); -int APS5_VABI sceSpPthreadMutexattrSettype(); -int APS5_VABI sceSpPthreadSetprio(); -int APS5_VABI sceSpSelect(); +int APS5_VABI sceSpKernelGettimeofday(KernelTimeval* tp); +int APS5_VABI sceSpKernelNanosleep(const KernelTimespec* rqtp, KernelTimespec* rmtp); +int APS5_VABI sceSpNetConnect(int s, const void* addr, std::uint32_t addrlen); +int APS5_VABI sceSpNetEpollAbort(int eid, int flags); +int APS5_VABI sceSpNetEpollControl(int eid, int op, int id, const void* event); +int APS5_VABI sceSpNetEpollCreate(const char* name, int flags); +int APS5_VABI sceSpNetEpollDestroy(int eid); +int APS5_VABI sceSpNetEpollWait(int eid, void* events, int maxevents, int timeout); +int* APS5_VABI sceSpNetErrnoLoc(); +int APS5_VABI sceSpNetPoolCreate(const char* name, int size, int flags); +int APS5_VABI sceSpNetPoolDestroy(int memid); +std::int64_t APS5_VABI sceSpNetRecv(int s, void* buf, std::size_t len, int flags); +int APS5_VABI sceSpNetResolverAbort(int rid, int flags); +int APS5_VABI sceSpNetResolverCreate(const char* name, int memid, int flags); +int APS5_VABI sceSpNetResolverDestroy(int rid); +int APS5_VABI sceSpNetResolverGetError(int rid, int* status); +int APS5_VABI sceSpNetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout, int retry, int flags); +std::int64_t APS5_VABI sceSpNetSend(int s, const void* buf, std::size_t len, int flags); +int APS5_VABI sceSpNetSetsockopt(int s, int level, int optname, const void* optval, std::uint32_t optlen); +int APS5_VABI sceSpNetSocket(const char* name, int family, int type, int protocol); +int APS5_VABI sceSpNetSocketClose(int s); +int APS5_VABI sceSpPthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI sceSpPthreadAttrInit(PthreadAttr* attr); +int APS5_VABI sceSpPthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched); +int APS5_VABI sceSpPthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); +int APS5_VABI sceSpPthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSize); +int APS5_VABI sceSpPthreadCondDestroy(PthreadCond* cond); +int APS5_VABI sceSpPthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char* name); +int APS5_VABI sceSpPthreadCondSignal(PthreadCond* cond); +int APS5_VABI sceSpPthreadCondWait(PthreadCond* cond, PthreadMutex* mutex); +int APS5_VABI sceSpPthreadCondattrDestroy(PthreadCondattr* attr); +int APS5_VABI sceSpPthreadCondattrInit(PthreadCondattr* attr); +int APS5_VABI sceSpPthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI sceSpPthreadDetach(Pthread thread); +void APS5_VABI sceSpPthreadExit(void* retval); +int APS5_VABI sceSpPthreadJoin(Pthread thread, void** retval); +int APS5_VABI sceSpPthreadMutexDestroy(PthreadMutex* mutex); +int APS5_VABI sceSpPthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name); +int APS5_VABI sceSpPthreadMutexLock(PthreadMutex* mutex); +int APS5_VABI sceSpPthreadMutexUnlock(PthreadMutex* mutex); +int APS5_VABI sceSpPthreadMutexattrDestroy(PthreadMutexattr* attr); +int APS5_VABI sceSpPthreadMutexattrInit(PthreadMutexattr* attr); +int APS5_VABI sceSpPthreadMutexattrSetprioceiling(PthreadMutexattr* attr, int prioceiling); +int APS5_VABI sceSpPthreadMutexattrSetprotocol(PthreadMutexattr* attr, int protocol); +int APS5_VABI sceSpPthreadMutexattrSettype(PthreadMutexattr* attr, int type); +int APS5_VABI sceSpPthreadSetprio(Pthread thread, int prio); +int APS5_VABI sceSpSelect(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout); } diff --git a/core/libs/prx/libSceSsl/CMakeLists.txt b/core/libs/prx/libSceSsl/CMakeLists.txt index cb39ff0e4c..8296a60735 100644 --- a/core/libs/prx/libSceSsl/CMakeLists.txt +++ b/core/libs/prx/libSceSsl/CMakeLists.txt @@ -90,9 +90,16 @@ if(BUILD_TESTING) target_link_libraries(guest_ssl_ps5_readers_tests PRIVATE libSceSsl libc) configure_windows_unwind(guest_ssl_ps5_readers_tests) add_test(NAME guest_ssl_ps5_readers COMMAND guest_ssl_ps5_readers_tests) + add_test_executable(guest_ssl_context_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/SslContextLifetime.cpp) + target_include_directories(guest_ssl_context_lifetime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ssl_context_lifetime_tests PRIVATE libc mbedtls mbedx509 mbedcrypto) + configure_windows_unwind(guest_ssl_context_lifetime_tests) + add_test(NAME guest_ssl_context_lifetime COMMAND guest_ssl_context_lifetime_tests) + set_tests_properties(guest_ssl_context_lifetime PROPERTIES TIMEOUT 30) if(WIN32) target_link_libraries(guest_ssl_transport_tests PRIVATE bcrypt ws2_32) - set_tests_properties(guest_ssl guest_ssl_load guest_ssl_ps5_readers guest_ssl_transport_kernel guest_ssl_transport_net PROPERTIES ENVIRONMENT_MODIFICATION + target_link_libraries(guest_ssl_context_lifetime_tests PRIVATE bcrypt ws2_32) + set_tests_properties(guest_ssl guest_ssl_load guest_ssl_ps5_readers guest_ssl_context_lifetime guest_ssl_transport_kernel guest_ssl_transport_net PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$;PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 9d3976dd63..c8a56dc09d 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -656,7 +656,9 @@ int APS5_VABI sceSslInit_nid_postfix(std::uint64_t poolSize) { int APS5_VABI sceSslTerm_nid_postfix(int sslCtxId) { auto& state = State(); std::lock_guard lock(state.mutex); - if (!state.contexts.erase(sslCtxId)) return ErrorInvalidId; + if (!state.contexts.contains(sslCtxId)) return ErrorInvalidId; + std::erase_if(state.connections, [sslCtxId](const auto& entry) { return entry.second.context == sslCtxId; }); + state.contexts.erase(sslCtxId); return 0; } @@ -996,6 +998,10 @@ int APS5_VABI sceSslRead(int sslConnectionId, void* buf, size_t len, int flags) return static_cast(received); } +int SslRead_nid_no_patch(int sslConnectionId, void* buffer, std::size_t length, int flags) { + return sceSslRead(sslConnectionId, buffer, length, flags); +} + int APS5_VABI sceSslWrite(int sslConnectionId, void* buf, size_t len, int flags) { if (flags != 0) return ErrorInvalidValue; if (!buf && len) throw std::invalid_argument("sceSslWrite: null buffer"); @@ -1261,7 +1267,6 @@ int APS5_VABI CRYPTO_initAsymmetricKey() { } int APS5_VABI CRYPTO_uninitAsymmetricKey() { - NotImplemented_nid_no_patch(__func__); return 0; } @@ -1271,7 +1276,6 @@ int APS5_VABI RSA_verifySignature() { } int APS5_VABI VLONG_freeVlongQueue() { - NotImplemented_nid_no_patch(__func__); return 0; } @@ -1353,4 +1357,9 @@ int APS5_VABI sceSslSetSslVersion() { return 0; } +int APS5_VABI sceSslSetServerName() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceSsl/include/Ssl.hpp b/core/libs/prx/libSceSsl/include/Ssl.hpp index 38e5e14589..5de6ba4c60 100644 --- a/core/libs/prx/libSceSsl/include/Ssl.hpp +++ b/core/libs/prx/libSceSsl/include/Ssl.hpp @@ -61,5 +61,6 @@ int APS5_VABI sceSslRecv(int sslConnectionId, void* buf, std::size_t fulllen); int APS5_VABI sceSslReuseConnection(int sslConnectionId, int sockId); int APS5_VABI sceSslSend(int sslConnectionId, void* msg, std::size_t fulllen); int APS5_VABI sceSslSetSslVersion(); +int APS5_VABI sceSslSetServerName(); } diff --git a/core/libs/prx/libSceSulphaDrv/CMakeLists.txt b/core/libs/prx/libSceSulphaDrv/CMakeLists.txt new file mode 100644 index 0000000000..649d75c4d0 --- /dev/null +++ b/core/libs/prx/libSceSulphaDrv/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceSulphaDrv SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceSulphaDrv PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceSulphaDrv) + +if(BUILD_TESTING) + add_test_executable(guest_sulpha_config_tests ${LIBS_INCLUDE_DIR}/tests/GuestSulphaConfig.cpp) + target_include_directories(guest_sulpha_config_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sulpha_config_tests PRIVATE libSceSulphaDrv libc) + configure_windows_unwind(guest_sulpha_config_tests) + add_test(NAME guest_sulpha_config COMMAND guest_sulpha_config_tests) + if(WIN32) + set_tests_properties(guest_sulpha_config PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$;PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceSulphaDrv/Export.cpp b/core/libs/prx/libSceSulphaDrv/Export.cpp index 328b7101a6..a40021ac2d 100644 --- a/core/libs/prx/libSceSulphaDrv/Export.cpp +++ b/core/libs/prx/libSceSulphaDrv/Export.cpp @@ -1,6 +1,7 @@ #include "prx/libSceSulphaDrv/include/Api.hpp" #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -17,8 +18,10 @@ int APS5_VABI sceSulphaGetConnectedClientCount(void) { return 0; } -int APS5_VABI sceSulphaGetDefaultConfig(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSulphaGetDefaultConfig(void* config) { + if (config == nullptr) return static_cast(0x804D0003u); + static constexpr std::uint64_t defaults[]{24, 0x400000, 0xFA100000003}; + std::memcpy(config, defaults, sizeof(defaults)); return 0; } diff --git a/core/libs/prx/libSceSulphaDrv/include/Api.hpp b/core/libs/prx/libSceSulphaDrv/include/Api.hpp index d04d50e887..86b1f74a4a 100644 --- a/core/libs/prx/libSceSulphaDrv/include/Api.hpp +++ b/core/libs/prx/libSceSulphaDrv/include/Api.hpp @@ -6,7 +6,7 @@ extern "C" { int APS5_VABI sceSulphaGetClientInfo(void); int APS5_VABI sceSulphaGetConnectedClientCount(void); -int APS5_VABI sceSulphaGetDefaultConfig(void); +int APS5_VABI sceSulphaGetDefaultConfig(void* config); int APS5_VABI sceSulphaGetNeededMemory(void); int APS5_VABI sceSulphaInit(void); int APS5_VABI sceSulphaMessage(void); diff --git a/core/libs/prx/libSceSysmodule/Export.cpp b/core/libs/prx/libSceSysmodule/Export.cpp index 272768fe00..6855f1f6af 100644 --- a/core/libs/prx/libSceSysmodule/Export.cpp +++ b/core/libs/prx/libSceSysmodule/Export.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include @@ -30,6 +32,10 @@ const char* findModuleName(const std::uint32_t id) { return it != kModuleTable.end() ? it->second : nullptr; } +bool isPublicModuleId(const std::uint16_t id) { + return std::binary_search(std::begin(kPublicModuleIds), std::end(kPublicModuleIds), id); +} + std::mutex gMutex; std::unordered_map gLoadCount; std::unordered_map gHandles; @@ -181,7 +187,7 @@ int APS5_VABI sceSysmoduleGetModuleHandleInternal(std::uint32_t id, std::int32_t } int APS5_VABI sceSysmoduleIsLoaded(std::uint16_t id) { - if (id == 0) { + if (!isPublicModuleId(id)) { return SCE_SYSMODULE_ERROR_INVALID_VALUE; } if (!findModuleName(id)) { @@ -197,7 +203,7 @@ int APS5_VABI sceSysmoduleIsLoaded(std::uint16_t id) { } int APS5_VABI sceSysmoduleLoadModule(std::uint16_t id) { - if (id == 0) { + if (!isPublicModuleId(id)) { return SCE_SYSMODULE_ERROR_INVALID_VALUE; } if (!findModuleName(id)) { @@ -228,7 +234,7 @@ int APS5_VABI sceSysmoduleLoadModuleInternalWithArg(std::uint32_t id, int argc, } int APS5_VABI sceSysmoduleUnloadModule(std::uint16_t id) { - if (id == 0) { + if (!isPublicModuleId(id)) { return SCE_SYSMODULE_ERROR_INVALID_VALUE; } if (!findModuleName(id)) { diff --git a/core/libs/prx/libSceSysmodule/ModuleTable.hpp b/core/libs/prx/libSceSysmodule/ModuleTable.hpp index eba12e75ac..b39a7d4e34 100644 --- a/core/libs/prx/libSceSysmodule/ModuleTable.hpp +++ b/core/libs/prx/libSceSysmodule/ModuleTable.hpp @@ -355,4 +355,19 @@ inline const std::unordered_map kModuleTable = { {0x800000c3, "libSceRnpsBundle"}, }; +inline constexpr std::uint16_t kPublicModuleIds[] = { + 0x0005, 0x0006, 0x0007, 0x000b, 0x0017, 0x0019, 0x001a, 0x001b, 0x001c, 0x001e, 0x0021, 0x002c, + 0x0038, 0x0081, 0x0083, 0x0084, 0x0085, 0x0088, 0x008a, 0x008b, 0x008c, 0x008d, 0x0095, 0x0096, + 0x0098, 0x0099, 0x009a, 0x009b, 0x009c, 0x009d, 0x009f, 0x00a0, 0x00a4, 0x00a5, 0x00a6, 0x00a7, + 0x00a8, 0x00a9, 0x00ab, 0x00ac, 0x00ad, 0x00ae, 0x00b4, 0x00b6, 0x00b7, 0x00b9, 0x00ba, 0x00bc, + 0x00bd, 0x00be, 0x00c0, 0x00c1, 0x00c2, 0x00c4, 0x00c6, 0x00c7, 0x00c8, 0x00c9, 0x00cc, 0x00cd, + 0x00ce, 0x00cf, 0x00d0, 0x00d1, 0x00d2, 0x00d3, 0x00d4, 0x00d5, 0x00d6, 0x00d8, 0x00da, 0x00de, + 0x00e1, 0x00e2, 0x00e3, 0x00e4, 0x00e5, 0x00e7, 0x00e9, 0x00ee, 0x00ef, 0x00f0, 0x00f1, 0x00fa, + 0x00fb, 0x00fe, 0x00ff, 0x0103, 0x0104, 0x0105, 0x0106, 0x0107, 0x010a, 0x010b, 0x010c, 0x010d, + 0x010e, 0x010f, 0x0110, 0x0111, 0x0112, 0x0113, 0x0114, 0x0115, 0x0116, 0x0117, 0x011a, 0x011b, + 0x011c, 0x011d, 0x011e, 0x011f, 0x0120, 0x0121, 0x0122, 0x0123, 0x0124, 0x0125, 0x0126, 0x0127, + 0x0128, 0x0129, 0x012a, 0x012b, 0x012c, 0x012d, 0x012e, 0x012f, 0x0130, 0x0132, 0x0133, 0x0134, + 0x0135, 0x0136, 0x0137, 0x0138, 0x0139, 0x013a, 0x013b, 0x013c, 0x013d, +}; + #endif diff --git a/core/libs/prx/libSceUlt/CMakeLists.txt b/core/libs/prx/libSceUlt/CMakeLists.txt index ea7e2f4a31..5736205384 100644 --- a/core/libs/prx/libSceUlt/CMakeLists.txt +++ b/core/libs/prx/libSceUlt/CMakeLists.txt @@ -41,11 +41,22 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test_executable(guest_ult_finalize_allocation_tests ${LIBS_INCLUDE_DIR}/tests/UltFinalizeAllocation.cpp Export.cpp src/ReaderWriterLock.cpp) + target_include_directories(guest_ult_finalize_allocation_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ult_finalize_allocation_tests PRIVATE libc libkernel) + configure_windows_unwind(guest_ult_finalize_allocation_tests) + add_test(NAME guest_ult_finalize_allocation COMMAND guest_ult_finalize_allocation_tests) + set_tests_properties(guest_ult_finalize_allocation PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_ult_finalize_allocation PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_ult_sync_tests ${LIBS_INCLUDE_DIR}/tests/GuestUltSync.cpp) target_include_directories(guest_ult_sync_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_ult_sync_tests PRIVATE libSceUlt libc) + target_link_libraries(guest_ult_sync_tests PRIVATE libSceUlt libc libkernel) configure_windows_unwind(guest_ult_sync_tests) - foreach(testCase IN ITEMS mutex-trylock opt-params rwlock-validation rwlock-readers rwlock-writer rwlock-pool rwlock-finalize ulthreads) + foreach(testCase IN ITEMS mutex-trylock opt-params rwlock-validation rwlock-readers rwlock-writer rwlock-pool rwlock-finalize ulthreads get-info queue-waiters info-waiters) add_test(NAME guest_ult_sync_${testCase} COMMAND guest_ult_sync_tests ${testCase}) set_tests_properties(guest_ult_sync_${testCase} PROPERTIES TIMEOUT 20) if(WIN32) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 88ac5556cc..3b55ed03e2 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -74,14 +74,15 @@ int QueueTransfer(void* queue, void* data, const void* source, bool push, bool b const auto ready = [&] { return !state->_alive || (push ? pool._free : state->_head) != UINT32_MAX; }; if (!ready()) { if (!blocking) return ULT_ERROR_AGAIN; - ++state->_waiters; + auto& waiters = push ? state->_waitingPushers : state->_waitingPoppers; + ++waiters; try { (push ? pool._spaceAvailable : state->_dataAvailable).wait(lock, ready); } catch (...) { - --state->_waiters; + --waiters; throw; } - --state->_waiters; + --waiters; if (!state->_alive) return ULT_ERROR_STATE; } if (push) { @@ -92,6 +93,7 @@ int QueueTransfer(void* queue, void* data, const void* source, bool push, bool b if (state->_tail == UINT32_MAX) state->_head = slot; else pool._next[state->_tail] = slot; state->_tail = slot; + ++state->_queuedItems; } else { const auto slot = state->_head; std::memcpy(data, pool._data.data() + slot * pool._dataSize, state->_dataSize); @@ -99,6 +101,7 @@ int QueueTransfer(void* queue, void* data, const void* source, bool push, bool b if (state->_head == UINT32_MAX) state->_tail = UINT32_MAX; pool._next[slot] = pool._free; pool._free = slot; + --state->_queuedItems; } lock.unlock(); if (push) state->_dataAvailable.notify_one(); @@ -113,6 +116,7 @@ void ReleaseQueue(UltQueueState& state) { pool._free = state._head; } state._head = state._tail = UINT32_MAX; + state._queuedItems = 0; state._alive = false; --pool._queues; } @@ -129,6 +133,14 @@ void* APS5_VABI ulthreadRunner(void* arg) { } +UltThreadIdentity UltCurrentThreadIdentity_nid_no_patch() { + if (gCurrentUlthread != nullptr) return {}; + const auto self = scePthreadSelf(); + std::uintptr_t thread; + std::memcpy(&thread, self, sizeof(thread)); + return {reinterpret_cast(thread), 1}; +} + bool UltWaitingPoolExists(void* pool) { std::lock_guard lock(gMutex); return gResourcePools.contains(pool); @@ -149,6 +161,7 @@ int APS5_VABI sceUltFinalize() { return ULT_ERROR_BUSY; } semaphores.reserve(gSemaphores.size()); + mutexes.reserve(gMutexes.size()); for (auto& entry : gSemaphores) { auto& state = entry.second; std::lock_guard stateLock(state->_mutex); @@ -156,7 +169,6 @@ int APS5_VABI sceUltFinalize() { semaphores.push_back(state); } gSemaphores.clear(); - mutexes.reserve(gMutexes.size()); for (auto& entry : gMutexes) { auto& state = entry.second; std::lock_guard stateLock(state->_mutex); @@ -243,7 +255,6 @@ int APS5_VABI _sceUltConditionVariableOptParamInitialize(UltOptParam* optParam, } int APS5_VABI sceUltMutexCreate(void* mutex, const char* name, void* waitingQueueResourcePool, const UltMutexOptParam* optParam, std::uint32_t buildVersion) { - (void)name; (void)buildVersion; if (mutex == nullptr) { return ULT_ERROR_NULL; @@ -251,6 +262,9 @@ int APS5_VABI sceUltMutexCreate(void* mutex, const char* name, void* waitingQueu auto state = std::make_shared(); state->_attribute = optParam != nullptr ? optParam->attribute : 0; state->_waitingPool = waitingQueueResourcePool; + if (name != nullptr) { + std::strncpy(state->_name, name, sizeof(state->_name) - 1); + } std::lock_guard lock(gMutex); if (waitingQueueResourcePool != nullptr && gResourcePools.find(waitingQueueResourcePool) == gResourcePools.end()) { return ULT_ERROR_INVALID; @@ -317,6 +331,7 @@ int APS5_VABI sceUltMutexLock(void* mutex) { if (!state->_alive) { return ULT_ERROR_STATE; } + state->_ownerIdentity = UltCurrentThreadIdentity_nid_no_patch(); state->_owner = self; state->_lockCount = 1; return ULT_OK; @@ -344,6 +359,7 @@ int APS5_VABI sceUltMutexTryLock(void* mutex) { if (state->_lockCount != 0) { return ULT_ERROR_BUSY; } + state->_ownerIdentity = UltCurrentThreadIdentity_nid_no_patch(); state->_owner = self; state->_lockCount = 1; return ULT_OK; @@ -370,13 +386,13 @@ int APS5_VABI sceUltMutexUnlock(void* mutex) { --state->_lockCount; if (state->_lockCount == 0) { state->_owner = {}; + state->_ownerIdentity = {}; state->_available.notify_one(); } return ULT_OK; } int APS5_VABI sceUltConditionVariableCreate(void* condition, const char* name, void* mutex, const void* optParam, std::uint32_t buildVersion) { - (void)name; (void)buildVersion; if (condition == nullptr || mutex == nullptr) return ULT_ERROR_NULL; if (optParam != nullptr) throw std::runtime_error("ULT condition variable options are not implemented"); @@ -386,6 +402,10 @@ int APS5_VABI sceUltConditionVariableCreate(void* condition, const char* name, v if (gConditions.contains(condition)) return ULT_ERROR_STATE; auto state = std::make_shared(); state->_mutex = mutexIt->second; + state->_mutexObject = mutex; + if (name != nullptr) { + std::strncpy(state->_name, name, sizeof(state->_name) - 1); + } std::lock_guard stateLock(state->_mutex->_mutex); if (!state->_mutex->_alive) return ULT_ERROR_STATE; gConditions.emplace(condition, state); @@ -442,12 +462,14 @@ int APS5_VABI sceUltConditionVariableWait(void* condition) { std::unique_lock lock(held->_mutex); if (!state->_alive || !held->_alive || held->_owner != owner || held->_lockCount == 0) return ULT_ERROR_STATE; if (held->_lockCount != 1) throw std::runtime_error("ULT condition wait with a recursively held mutex is not implemented"); + const auto ownerIdentity = held->_ownerIdentity; UltConditionWaiter waiter; state->_queue.push_back(&waiter); ++state->_waiters; ++held->_conditionWaiters; held->_lockCount = 0; held->_owner = {}; + held->_ownerIdentity = {}; held->_available.notify_one(); try { waiter._available.wait(lock, [&] { return waiter._signaled || !state->_alive || !held->_alive; }); @@ -472,6 +494,7 @@ int APS5_VABI sceUltConditionVariableWait(void* condition) { --held->_conditionWaiters; if (!held->_alive || !state->_alive) return ULT_ERROR_STATE; held->_owner = owner; + held->_ownerIdentity = ownerIdentity; held->_lockCount = 1; return ULT_OK; } @@ -481,15 +504,18 @@ std::uint64_t APS5_VABI sceUltWaitingQueueResourcePoolGetWorkAreaSize(std::uint3 } int APS5_VABI sceUltWaitingQueueResourcePoolCreate(void* pool, const char* name, std::uint32_t numThreads, std::uint32_t numSyncObjects, void* workArea, const void* optParam, std::uint32_t buildVersion) { - (void)name; (void)optParam; (void)buildVersion; if (pool == nullptr) { return ULT_ERROR_NULL; } std::memset(pool, 0, 256); + UltResourcePoolState state{numThreads, numSyncObjects, workArea}; + if (name != nullptr) { + std::strncpy(state._name, name, sizeof(state._name) - 1); + } std::lock_guard lock(gMutex); - gResourcePools[pool] = {numThreads, numSyncObjects, workArea}; + gResourcePools[pool] = state; return ULT_OK; } @@ -521,7 +547,7 @@ int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void* pool) { for (const auto& entry : gQueues) { if (entry.second->_pool && entry.second->_pool->_waitingPool == pool) { std::lock_guard poolLock(entry.second->_pool->_mutex); - if (entry.second->_waiters != 0) { + if (entry.second->_waitingPushers != 0 || entry.second->_waitingPoppers != 0) { return ULT_ERROR_STATE; } } @@ -542,7 +568,6 @@ std::uint64_t APS5_VABI sceUltQueueDataResourcePoolGetWorkAreaSize(std::uint32_t } int APS5_VABI sceUltQueueDataResourcePoolCreate(void* pool, const char* name, std::uint32_t numData, std::uint64_t dataSize, std::uint32_t numQueueObject, void* waitingQueueResourcePool, void* workArea, const void* optParam, std::uint32_t buildVersion) { - (void)name; (void)optParam; (void)buildVersion; if (pool == nullptr) { @@ -562,6 +587,9 @@ int APS5_VABI sceUltQueueDataResourcePoolCreate(void* pool, const char* name, st state->_numQueueObject = numQueueObject; state->_waitingPool = waitingQueueResourcePool; state->_workArea = workArea; + if (name != nullptr) { + std::strncpy(state->_name, name, sizeof(state->_name) - 1); + } state->_data.resize(numData * dataSize); state->_next.resize(numData); for (std::uint32_t slot = 0; slot < numData; ++slot) { @@ -586,7 +614,6 @@ int APS5_VABI sceUltQueueDataResourcePoolDestroy(void* pool) { } int APS5_VABI sceUltQueueCreate(void* queue, const char* name, std::uint64_t dataSize, void* waitingQueueResourcePool, void* queueDataResourcePool, const void* optParam, std::uint32_t buildVersion) { - (void)name; (void)optParam; (void)buildVersion; if (queue == nullptr) { @@ -596,6 +623,11 @@ int APS5_VABI sceUltQueueCreate(void* queue, const char* name, std::uint64_t dat if (dataSize == 0) return ULT_ERROR_RANGE; auto state = std::make_shared(); state->_dataSize = dataSize; + state->_waitingPool = waitingQueueResourcePool; + state->_dataPool = queueDataResourcePool; + if (name != nullptr) { + std::strncpy(state->_name, name, sizeof(state->_name) - 1); + } std::lock_guard lock(gMutex); auto dataPoolIt = gQueueDataPools.find(queueDataResourcePool); if (dataPoolIt == gQueueDataPools.end()) { @@ -641,7 +673,7 @@ int APS5_VABI sceUltQueueDestroy(void* queue) { auto& pool = *state->_pool; { std::lock_guard stateLock(pool._mutex); - if (state->_waiters != 0) return ULT_ERROR_STATE; + if (state->_waitingPushers != 0 || state->_waitingPoppers != 0) return ULT_ERROR_STATE; ReleaseQueue(*state); gQueues.erase(it); } @@ -650,7 +682,6 @@ int APS5_VABI sceUltQueueDestroy(void* queue) { } int APS5_VABI sceUltSemaphoreCreate(void* semaphore, const char* name, std::uint32_t numInitialResource, void* waitingQueueResourcePool, const void* optParam, std::uint32_t buildVersion) { - (void)name; (void)optParam; (void)buildVersion; if (semaphore == nullptr) { @@ -662,6 +693,9 @@ int APS5_VABI sceUltSemaphoreCreate(void* semaphore, const char* name, std::uint auto state = std::make_shared(); state->_resources = std::bit_cast(numInitialResource); state->_waitingPool = waitingQueueResourcePool; + if (name != nullptr) { + std::strncpy(state->_name, name, sizeof(state->_name) - 1); + } std::lock_guard lock(gMutex); if (waitingQueueResourcePool != nullptr && gResourcePools.find(waitingQueueResourcePool) == gResourcePools.end()) { return ULT_ERROR_INVALID; @@ -779,15 +813,18 @@ std::uint64_t APS5_VABI sceUltUlthreadRuntimeGetWorkAreaSize(std::uint32_t maxNu } int APS5_VABI sceUltUlthreadRuntimeCreate(void* runtime, const char* name, std::uint32_t maxNumUlthread, std::uint32_t numWorkerThread, void* workArea, const void* optParam, std::uint32_t buildVersion) { - (void)name; (void)optParam; (void)buildVersion; if (runtime == nullptr) { return ULT_ERROR_NULL; } std::memset(runtime, 0, 4096); + UltRuntimeState state{maxNumUlthread, numWorkerThread, workArea}; + if (name != nullptr) { + std::strncpy(state._name, name, sizeof(state._name) - 1); + } std::lock_guard lock(gMutex); - gRuntimes[runtime] = {maxNumUlthread, numWorkerThread, workArea}; + gRuntimes[runtime] = state; return ULT_OK; } @@ -804,6 +841,9 @@ int APS5_VABI sceUltUlthreadCreate(void* ulthread, const char* name, UltUlthread state->_arg = arg; state->_object = ulthread; state->_runtime = runtime; + if (name != nullptr) { + std::strncpy(state->_name, name, sizeof(state->_name) - 1); + } { std::lock_guard lock(gMutex); if (gRuntimes.find(runtime) == gRuntimes.end()) { @@ -992,48 +1032,194 @@ int APS5_VABI sceUltConditionVariableSignalAll(void* conditionVariable) { } int APS5_VABI sceUltGetConditionVariableInfo(void* object, UltConditionVariableInfo* info, UltWaitingThreadInfo* waitingThreads, std::uint32_t capacity) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (object == nullptr || info == nullptr) return ULT_ERROR_NULL; + if (waitingThreads == nullptr && capacity != 0) return ULT_ERROR_NULL; + std::shared_ptr state; + { + std::lock_guard lock(gMutex); + const auto it = gConditions.find(object); + if (it == gConditions.end()) return ULT_ERROR_STATE; + state = it->second; + } + std::lock_guard stateLock(state->_mutex->_mutex); + if (!state->_alive) return ULT_ERROR_STATE; + if (capacity != 0 && !state->_queue.empty()) NotImplemented_nid_no_patch(__func__); + std::memset(info, 0, sizeof(*info)); + info->size = sizeof(UltConditionVariableInfo); + info->object = object; + std::memcpy(info->name, state->_name, sizeof(info->name)); + info->mutex = state->_mutexObject; + info->waitingThreads = static_cast(state->_queue.size()); + return ULT_OK; } int APS5_VABI sceUltGetMutexInfo(void* object, UltMutexInfo* info, UltWaitingThreadInfo* waitingThreads, std::uint32_t capacity) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (object == nullptr || info == nullptr) return ULT_ERROR_NULL; + if (waitingThreads == nullptr && capacity != 0) return ULT_ERROR_NULL; + std::shared_ptr state; + { + std::lock_guard lock(gMutex); + const auto it = gMutexes.find(object); + if (it == gMutexes.end()) return ULT_ERROR_STATE; + state = it->second; + } + std::lock_guard stateLock(state->_mutex); + if (!state->_alive) return ULT_ERROR_STATE; + for (std::uint32_t index = 0; index < capacity; ++index) { + if (waitingThreads[index].size != sizeof(UltWaitingThreadInfo)) return ULT_ERROR_INVALID; + } + if (capacity != 0 && state->_waiters != 0) NotImplemented_nid_no_patch(__func__); + if (state->_lockCount != 0 && (state->_attribute != 0 || state->_lockCount > 1 || state->_ownerIdentity.type != 1)) NotImplemented_nid_no_patch(__func__); + std::memset(info, 0, sizeof(*info)); + info->size = sizeof(UltMutexInfo); + info->object = object; + std::memcpy(info->name, state->_name, sizeof(info->name)); + info->waitingQueuePool = state->_waitingPool; + info->owner = state->_ownerIdentity.thread; + info->ownerType = state->_ownerIdentity.type; + info->attributes = state->_attribute; + info->waitingThreads = state->_waiters; + return ULT_OK; } int APS5_VABI sceUltGetQueueDataResourcePoolInfo(void* object, UltQueueDataResourcePoolInfo* info) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (object == nullptr || info == nullptr) return ULT_ERROR_NULL; + std::shared_ptr state; + { + std::lock_guard lock(gMutex); + const auto it = gQueueDataPools.find(object); + if (it == gQueueDataPools.end()) return ULT_ERROR_STATE; + state = it->second; + } + std::lock_guard poolLock(state->_mutex); + std::memset(info, 0, sizeof(*info)); + info->size = sizeof(UltQueueDataResourcePoolInfo); + info->object = object; + std::memcpy(info->name, state->_name, sizeof(info->name)); + info->waitingQueuePool = state->_waitingPool; + info->dataSize = state->_dataSize; + info->maxItems = state->_numData; + info->maxQueues = state->_numQueueObject; + info->availableQueues = state->_numQueueObject > state->_queues ? state->_numQueueObject - state->_queues : 0; + std::uint32_t freeCount = 0; + for (auto next = state->_free; next != UINT32_MAX; next = state->_next[next]) { + ++freeCount; + } + info->availableItems = freeCount; + return ULT_OK; } int APS5_VABI sceUltGetQueueInfo(void* object, UltQueueInfo* info, UltWaitingThreadInfo* waitingThreads, std::uint32_t capacity) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltGetReaderWriterLockInfo(void* object, UltReaderWriterLockInfo* info, UltWaitingThreadInfo* waitingThreads, std::uint32_t capacity) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (object == nullptr || info == nullptr) return ULT_ERROR_NULL; + if (waitingThreads == nullptr && capacity != 0) return ULT_ERROR_NULL; + std::shared_ptr state; + { + std::lock_guard lock(gMutex); + const auto it = gQueues.find(object); + if (it == gQueues.end()) return ULT_ERROR_STATE; + state = it->second; + } + auto& pool = *state->_pool; + std::lock_guard poolLock(pool._mutex); + if (!state->_alive) return ULT_ERROR_STATE; + if (capacity != 0 && (state->_waitingPushers != 0 || state->_waitingPoppers != 0)) NotImplemented_nid_no_patch(__func__); + std::memset(info, 0, sizeof(*info)); + info->size = sizeof(UltQueueInfo); + info->object = object; + std::memcpy(info->name, state->_name, sizeof(info->name)); + info->waitingQueuePool = state->_waitingPool; + info->dataPool = state->_dataPool; + info->dataSize = state->_dataSize; + info->queuedItems = state->_queuedItems; + info->waitingPushers = state->_waitingPushers; + info->waitingPoppers = state->_waitingPoppers; + return ULT_OK; } int APS5_VABI sceUltGetSemaphoreInfo(void* object, UltSemaphoreInfo* info, UltWaitingThreadInfo* waitingThreads, std::uint32_t capacity) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (object == nullptr || info == nullptr) return ULT_ERROR_NULL; + if (waitingThreads == nullptr && capacity != 0) return ULT_ERROR_NULL; + std::shared_ptr state; + { + std::lock_guard lock(gMutex); + const auto it = gSemaphores.find(object); + if (it == gSemaphores.end()) return ULT_ERROR_STATE; + state = it->second; + } + std::lock_guard stateLock(state->_mutex); + if (!state->_alive) return ULT_ERROR_STATE; + if (capacity != 0 && state->_waiters != 0) NotImplemented_nid_no_patch(__func__); + std::memset(info, 0, sizeof(*info)); + info->size = sizeof(UltSemaphoreInfo); + info->object = object; + std::memcpy(info->name, state->_name, sizeof(info->name)); + info->waitingQueuePool = state->_waitingPool; + info->resources = static_cast(state->_resources); + info->waitingThreads = state->_waiters; + return ULT_OK; } int APS5_VABI sceUltGetUlthreadInfo(void* object, UltUlthreadInfo* info) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (object == nullptr || info == nullptr) return ULT_ERROR_NULL; + std::shared_ptr state; + { + std::lock_guard lock(gMutex); + const auto it = gUlthreads.find(object); + if (it == gUlthreads.end()) return ULT_ERROR_STATE; + state = it->second; + } + std::memset(info, 0, sizeof(*info)); + info->size = sizeof(UltUlthreadInfo); + info->object = object; + std::memcpy(info->name, state->_name, sizeof(info->name)); + info->runtime = state->_runtime; + info->contextAttribute = state->_contextAttribute; + return ULT_OK; } int APS5_VABI sceUltGetUlthreadRuntimeInfo(void* runtime, UltUlthreadRuntimeInfo* info, void** ulthreads, std::uint32_t ulthreadCapacity, Pthread* workerThreads, std::uint32_t workerCapacity) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (runtime == nullptr || info == nullptr) return ULT_ERROR_NULL; + if (ulthreads == nullptr && ulthreadCapacity != 0) return ULT_ERROR_NULL; + if (workerThreads == nullptr && workerCapacity != 0) return ULT_ERROR_NULL; + if (workerThreads != nullptr && workerCapacity != 0) throw std::runtime_error("ULT ulthread runtime worker thread handles are not implemented"); + std::lock_guard lock(gMutex); + const auto it = gRuntimes.find(runtime); + if (it == gRuntimes.end()) return ULT_ERROR_STATE; + const auto& state = it->second; + std::memset(info, 0, sizeof(*info)); + info->size = sizeof(UltUlthreadRuntimeInfo); + info->object = runtime; + std::memcpy(info->name, state._name, sizeof(info->name)); + info->maxUlthreads = state._maxNumUlthread; + info->workerThreads = state._numWorkerThread; + std::uint32_t count = 0; + for (const auto& [obj, ulthread] : gUlthreads) { + if (ulthread->_runtime == runtime && !ulthread->_exited.load(std::memory_order_acquire)) { + if (ulthreads != nullptr && count < ulthreadCapacity) { + ulthreads[count] = obj; + } + ++count; + } + } + info->activeUlthreads = count; + return ULT_OK; } int APS5_VABI sceUltGetWaitingQueueResourcePoolInfo(void* object, UltWaitingQueueResourcePoolInfo* info) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (object == nullptr || info == nullptr) return ULT_ERROR_NULL; + std::lock_guard lock(gMutex); + const auto it = gResourcePools.find(object); + if (it == gResourcePools.end()) return ULT_ERROR_STATE; + const auto& state = it->second; + std::memset(info, 0, sizeof(*info)); + info->size = sizeof(UltWaitingQueueResourcePoolInfo); + info->object = object; + std::memcpy(info->name, state._name, sizeof(info->name)); + info->maxThreads = state._numThreads; + info->availableThreads = state._numThreads; + info->maxSyncObjects = state._numSyncObjects; + info->availableSyncObjects = state._numSyncObjects; + return ULT_OK; } int APS5_VABI sceUltAbstractThreadFactoryCreateThread() { diff --git a/core/libs/prx/libSceUlt/SceUltTypes.hpp b/core/libs/prx/libSceUlt/SceUltTypes.hpp index 4c77af1649..f218503bbc 100644 --- a/core/libs/prx/libSceUlt/SceUltTypes.hpp +++ b/core/libs/prx/libSceUlt/SceUltTypes.hpp @@ -265,10 +265,16 @@ static_assert(offsetof(UltUlthreadInfo, runtime) == 56); static_assert(offsetof(UltUlthreadInfo, fiber) == 64); static_assert(offsetof(UltUlthreadInfo, reserved) == 192); +struct UltThreadIdentity { + void* thread = nullptr; + std::uint32_t type = 0; +}; + struct UltMutexState { std::mutex _mutex; std::condition_variable _available; std::thread::id _owner{}; + UltThreadIdentity _ownerIdentity; std::uint32_t _lockCount = 0; std::uint32_t _waiters = 0; std::uint32_t _conditions = 0; @@ -276,6 +282,7 @@ struct UltMutexState { bool _alive = true; std::uint32_t _attribute = 0; void* _waitingPool = nullptr; + char _name[32]{}; }; struct UltConditionWaiter { @@ -288,6 +295,8 @@ struct UltConditionState { std::list _queue; std::uint32_t _waiters = 0; bool _alive = true; + void* _mutexObject = nullptr; + char _name[32]{}; }; struct UltSemaphoreState { @@ -297,12 +306,14 @@ struct UltSemaphoreState { std::uint32_t _waiters = 0; bool _alive = true; void* _waitingPool = nullptr; + char _name[32]{}; }; struct UltResourcePoolState { std::uint32_t _numThreads = 0; std::uint32_t _numSyncObjects = 0; void* _workArea = nullptr; + char _name[32]{}; }; struct UltQueueDataPoolState { @@ -317,6 +328,7 @@ struct UltQueueDataPoolState { std::uint32_t _numQueueObject = 0; void* _waitingPool = nullptr; void* _workArea = nullptr; + char _name[32]{}; }; struct UltQueueState { @@ -324,15 +336,21 @@ struct UltQueueState { std::condition_variable _dataAvailable; std::uint32_t _head = UINT32_MAX; std::uint32_t _tail = UINT32_MAX; - std::uint32_t _waiters = 0; + std::uint32_t _waitingPushers = 0; + std::uint32_t _waitingPoppers = 0; bool _alive = true; std::uint64_t _dataSize = 0; + void* _waitingPool = nullptr; + void* _dataPool = nullptr; + std::uint32_t _queuedItems = 0; + char _name[32]{}; }; struct UltRuntimeState { std::uint32_t _maxNumUlthread = 0; std::uint32_t _numWorkerThread = 0; void* _workArea = nullptr; + char _name[32]{}; }; struct UltUlthreadState { @@ -342,6 +360,8 @@ struct UltUlthreadState { Pthread _thread = nullptr; void* _runtime = nullptr; std::atomic _exited{false}; + char _name[32]{}; + std::uint32_t _contextAttribute = 0; }; #endif diff --git a/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp b/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp index ba86691ca6..489f3587ea 100644 --- a/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp +++ b/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp @@ -1,6 +1,8 @@ #include "prx/libSceUlt/include/Api.hpp" #include "prx/libSceUlt/src/ReaderWriterLock.hpp" #include "prx/libSceUlt/SceUltTypes.hpp" +#include "prx/libc/include/General.hpp" +#include #include #include #include @@ -17,9 +19,11 @@ struct UltReaderWriterLockState { std::uint32_t _readers = 0; bool _writing = false; std::thread::id _writer{}; + UltThreadIdentity _ownerIdentity; std::uint32_t _waiters = 0; bool _alive = true; void* _waitingPool = nullptr; + char _name[32]{}; }; std::mutex gLocksMutex; @@ -79,7 +83,6 @@ int APS5_VABI sceUltReaderWriterLockOptParamInitialize(UltOptParam* optParam, st } int APS5_VABI sceUltReaderWriterLockCreate(void* rwlock, const char* name, void* waitingQueueResourcePool, const UltOptParam* optParam, std::uint32_t buildVersion) { - (void)name; (void)optParam; (void)buildVersion; if (rwlock == nullptr) { @@ -90,6 +93,9 @@ int APS5_VABI sceUltReaderWriterLockCreate(void* rwlock, const char* name, void* } auto state = std::make_shared(); state->_waitingPool = waitingQueueResourcePool; + if (name != nullptr) { + std::strncpy(state->_name, name, sizeof(state->_name) - 1); + } std::lock_guard lock(gLocksMutex); if (gLocks.contains(rwlock)) { return ULT_ERROR_STATE; @@ -166,6 +172,7 @@ int APS5_VABI sceUltReaderWriterLockLockWrite(void* rwlock) { return ULT_ERROR_STATE; } if (const int waited = Wait(*state, lock, true); waited != ULT_OK) return waited; + state->_ownerIdentity = UltCurrentThreadIdentity_nid_no_patch(); state->_writing = true; state->_writer = std::this_thread::get_id(); return ULT_OK; @@ -181,6 +188,7 @@ int APS5_VABI sceUltReaderWriterLockTryLockWrite(void* rwlock) { if (state->_writing || state->_readers != 0) { return ULT_ERROR_BUSY; } + state->_ownerIdentity = UltCurrentThreadIdentity_nid_no_patch(); state->_writing = true; state->_writer = std::this_thread::get_id(); return ULT_OK; @@ -195,8 +203,34 @@ int APS5_VABI sceUltReaderWriterLockUnlockWrite(void* rwlock) { } state->_writing = false; state->_writer = {}; + state->_ownerIdentity = {}; state->_released.notify_all(); return ULT_OK; } +int APS5_VABI sceUltGetReaderWriterLockInfo(void* object, UltReaderWriterLockInfo* info, UltWaitingThreadInfo* waitingThreads, std::uint32_t capacity) { + if (object == nullptr || info == nullptr) return ULT_ERROR_NULL; + if (waitingThreads == nullptr && capacity != 0) return ULT_ERROR_NULL; + std::shared_ptr state; + if (const int found = FindLock(object, state); found != ULT_OK) return found; + std::lock_guard lock(state->_mutex); + if (!state->_alive) return ULT_ERROR_STATE; + for (std::uint32_t index = 0; index < capacity; ++index) { + if (waitingThreads[index].size != sizeof(UltWaitingThreadInfo)) return ULT_ERROR_INVALID; + } + if (capacity != 0 && state->_waiters != 0) NotImplemented_nid_no_patch(__func__); + if (state->_writing && state->_ownerIdentity.type != 1) NotImplemented_nid_no_patch(__func__); + std::memset(info, 0, sizeof(*info)); + info->size = sizeof(UltReaderWriterLockInfo); + info->object = object; + std::memcpy(info->name, state->_name, sizeof(info->name)); + info->waitingQueuePool = state->_waitingPool; + info->readers = state->_readers; + info->waitingThreads = state->_waiters; + info->lockType = state->_writing ? 2 : (state->_readers > 0 ? 1 : 0); + info->owner = state->_ownerIdentity.thread; + info->ownerType = state->_ownerIdentity.type; + return ULT_OK; +} + } diff --git a/core/libs/prx/libSceUlt/src/ReaderWriterLock.hpp b/core/libs/prx/libSceUlt/src/ReaderWriterLock.hpp index df98dcd077..b8c0b7d704 100644 --- a/core/libs/prx/libSceUlt/src/ReaderWriterLock.hpp +++ b/core/libs/prx/libSceUlt/src/ReaderWriterLock.hpp @@ -1,6 +1,9 @@ #ifndef CORE_LIBS_PRX_LIBSCEULT_SRC_READERWRITERLOCK_HPP #define CORE_LIBS_PRX_LIBSCEULT_SRC_READERWRITERLOCK_HPP +struct UltThreadIdentity; + +UltThreadIdentity UltCurrentThreadIdentity_nid_no_patch(); bool UltWaitingPoolExists(void* pool); bool UltReaderWriterLocksWaitOn(void* pool); void UltFinalizeReaderWriterLocks(); diff --git a/core/libs/prx/libSceUsbd/Export.cpp b/core/libs/prx/libSceUsbd/Export.cpp index 46219b9973..edc6a19de0 100644 --- a/core/libs/prx/libSceUsbd/Export.cpp +++ b/core/libs/prx/libSceUsbd/Export.cpp @@ -6,6 +6,7 @@ #include +#include #include #include #include @@ -47,6 +48,7 @@ struct UsbdState { std::unordered_map devices; std::unordered_set lists; std::unordered_set handles; + std::unordered_map claimed; std::unordered_set configs; std::unordered_map inFlight; }; @@ -72,11 +74,15 @@ std::int32_t ToGuestError(int result) { return static_cast(0x80240000u - static_cast(result)); } +bool IsKnownDevice(const UsbdState& state, libusb_device* device) { + return state.devices.contains(device) || std::any_of(state.handles.begin(), state.handles.end(), [device](libusb_device_handle* handle) { return libusb_get_device(handle) == device; }); +} + libusb_device* KnownDevice(void* device, const char* function) { auto& state = State(); std::lock_guard lock(state.mutex); auto* host = static_cast(device); - if (!state.devices.contains(host)) throw std::runtime_error(AbsentDevice(function)); + if (!IsKnownDevice(state, host)) throw std::runtime_error(AbsentDevice(function)); return host; } @@ -115,6 +121,27 @@ void LIBUSB_CALL CompleteTransfer(libusb_transfer* host) { if (guest->callback != nullptr) guest->callback(guest); } +int MaxPacketSize(libusb_device* device, std::uint8_t endpoint) { + libusb_config_descriptor* config = nullptr; + const int result = libusb_get_active_config_descriptor(device, &config); + if (result < 0) return result; + int size = LIBUSB_ERROR_NOT_FOUND; + for (int index = 0; index < config->bNumInterfaces && size == LIBUSB_ERROR_NOT_FOUND; ++index) { + const auto& entry = config->interface[index]; + for (int alternate = 0; alternate < entry.num_altsetting && size == LIBUSB_ERROR_NOT_FOUND; ++alternate) { + const auto& setting = entry.altsetting[alternate]; + for (int item = 0; item < setting.bNumEndpoints; ++item) { + if (setting.endpoint[item].bEndpointAddress == endpoint) { + size = setting.endpoint[item].wMaxPacketSize; + break; + } + } + } + } + libusb_free_config_descriptor(config); + return size; +} + bool RequestStringDescriptor(libusb_device_handle* handle, std::uint8_t descriptorIndex, std::uint16_t languageId, std::uint8_t* descriptor) { std::memset(descriptor, 0, STRING_DESCRIPTOR_SIZE); const auto value = static_cast(LIBUSB_DT_STRING << 8 | descriptorIndex); @@ -143,6 +170,7 @@ void APS5_VABI sceUsbdExit() { if (state.initCount == 0 || --state.initCount != 0) return; if (!state.inFlight.empty()) throw std::runtime_error(std::string(__func__) + ": transfers are still in flight"); for (auto* handle : state.handles) libusb_close(handle); + state.claimed.clear(); for (auto* config : state.configs) libusb_free_config_descriptor(config); for (auto* list : state.lists) libusb_free_device_list(list, 0); for (const auto& [device, references] : state.devices) { @@ -358,7 +386,14 @@ std::int32_t APS5_VABI sceUsbdCheckConnected(void* deviceHandle) { std::int32_t APS5_VABI sceUsbdClaimInterface(void* deviceHandle, std::int32_t interfaceNumber) { if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; - return ToGuestError(libusb_claim_interface(KnownHandle(deviceHandle, __func__), interfaceNumber)); + auto* handle = KnownHandle(deviceHandle, __func__); + const int result = libusb_claim_interface(handle, interfaceNumber); + if (result == 0) { + auto& state = State(); + std::lock_guard lock(state.mutex); + state.claimed[handle] |= 1u << interfaceNumber; + } + return ToGuestError(result); } void APS5_VABI sceUsbdClose(void* deviceHandle) { @@ -367,6 +402,7 @@ void APS5_VABI sceUsbdClose(void* deviceHandle) { std::lock_guard lock(state.mutex); auto* host = static_cast(deviceHandle); if (state.handles.erase(host) == 0) throw std::runtime_error(UnopenedHandle(__func__)); + state.claimed.erase(host); libusb_close(host); } @@ -449,14 +485,21 @@ void* APS5_VABI sceUsbdRefDevice(void* device) { auto& state = State(); std::lock_guard lock(state.mutex); auto* host = static_cast(device); - if (!state.devices.contains(host)) throw std::runtime_error(AbsentDevice(__func__)); + if (!IsKnownDevice(state, host)) throw std::runtime_error(AbsentDevice(__func__)); AddDeviceReference(state, host); return libusb_ref_device(host); } std::int32_t APS5_VABI sceUsbdReleaseInterface(void* deviceHandle, std::int32_t interfaceNumber) { if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; - return ToGuestError(libusb_release_interface(KnownHandle(deviceHandle, __func__), interfaceNumber)); + auto* handle = KnownHandle(deviceHandle, __func__); + const int result = libusb_release_interface(handle, interfaceNumber); + if (result == 0) { + auto& state = State(); + std::lock_guard lock(state.mutex); + state.claimed[handle] &= ~(1u << interfaceNumber); + } + return ToGuestError(result); } std::int32_t APS5_VABI sceUsbdResetDevice(void* deviceHandle) { @@ -516,13 +559,16 @@ void APS5_VABI sceUsbdUnrefDevice(void* device) { } std::int32_t APS5_VABI sceUsbdClearHalt(void* deviceHandle, std::uint8_t endpoint) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (deviceHandle == nullptr) throw std::runtime_error(std::string(__func__) + ": null handle"); + const int result = libusb_clear_halt(KnownHandle(deviceHandle, __func__), endpoint); + if (result == 0) return 0; + if (result == LIBUSB_ERROR_NOT_FOUND) return SCE_USBD_ERROR_OTHER; + throw std::runtime_error(std::string(__func__) + ": libusb error " + std::to_string(result) + " has no known console result"); } std::int32_t APS5_VABI sceUsbdDetachKernelDriver(void* deviceHandle, int interfaceNumber) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + return libusb_detach_kernel_driver(KnownHandle(deviceHandle, __func__), static_cast(interfaceNumber)) == 0 ? 0 : SCE_USBD_ERROR_OTHER; } std::int32_t APS5_VABI sceUsbdEventHandlerActive() { @@ -559,23 +605,27 @@ std::int32_t APS5_VABI sceUsbdGetDescriptor(void* deviceHandle, std::uint8_t des } void* APS5_VABI sceUsbdGetDevice(void* deviceHandle) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (deviceHandle == nullptr) return nullptr; + return libusb_get_device(KnownHandle(deviceHandle, __func__)); } int APS5_VABI sceUsbdGetDeviceSpeed(void* device) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (device == nullptr) return LIBUSB_SPEED_UNKNOWN; + return std::min(libusb_get_device_speed(KnownDevice(device, __func__)), static_cast(LIBUSB_SPEED_SUPER)); } std::int32_t APS5_VABI sceUsbdGetMaxIsoPacketSize(void* device, std::uint8_t endpoint) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (device == nullptr) throw std::runtime_error(std::string(__func__) + ": null device"); + auto* host = KnownDevice(device, __func__); + int size = MaxPacketSize(host, endpoint); + const int speed = libusb_get_device_speed(host); + if (speed != LIBUSB_SPEED_LOW && speed != LIBUSB_SPEED_FULL && size >= 0) size = (size & 0x7FF) * std::min(1 + ((size >> 11) & 3), 3); + return ToGuestError(size); } std::int32_t APS5_VABI sceUsbdGetMaxPacketSize(void* device, std::uint8_t endpoint) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (device == nullptr) return ToGuestError(LIBUSB_ERROR_NO_DEVICE); + return ToGuestError(MaxPacketSize(KnownDevice(device, __func__), endpoint)); } std::int32_t APS5_VABI sceUsbdGetStringDescriptorAscii(void* deviceHandle, std::uint8_t descriptorIndex, std::uint8_t* data, std::int32_t length) { @@ -629,13 +679,39 @@ void APS5_VABI sceUsbdLockEvents() { } void* APS5_VABI sceUsbdOpenDeviceWithVidPid(std::uint16_t vendorId, std::uint16_t productId) { - NotImplemented_nid_no_patch(__func__); - return 0; + auto& state = State(); + std::lock_guard lock(state.mutex); + if (state.context == nullptr) return nullptr; + libusb_device** devices = nullptr; + const ssize_t count = libusb_get_device_list(state.context, &devices); + if (count < 0) return nullptr; + libusb_device_handle* handle = nullptr; + for (ssize_t index = 0; index < count; ++index) { + libusb_device_descriptor descriptor{}; + if (libusb_get_device_descriptor(devices[index], &descriptor) == 0 && descriptor.idVendor == vendorId && descriptor.idProduct == productId) { + if (libusb_open(devices[index], &handle) != 0) handle = nullptr; + break; + } + } + libusb_free_device_list(devices, 1); + if (handle != nullptr) state.handles.insert(handle); + return handle; } std::int32_t APS5_VABI sceUsbdSetInterfaceAltSetting(void* deviceHandle, int interfaceNumber, int alternateSetting) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + auto* handle = KnownHandle(deviceHandle, __func__); + if (interfaceNumber < 0 || interfaceNumber > 31) return SCE_USBD_ERROR_INVALID_ARG; + { + auto& state = State(); + std::lock_guard lock(state.mutex); + const auto claimed = state.claimed.find(handle); + if (claimed == state.claimed.end() || (claimed->second & (1u << interfaceNumber)) == 0) return ToGuestError(LIBUSB_ERROR_NOT_FOUND); + for (const auto& [guest, host] : state.inFlight) { + if (host->dev_handle == handle) throw std::runtime_error(std::string(__func__) + ": the device has transfers in flight, which FreeBSD libusb cancels first"); + } + } + return libusb_set_interface_alt_setting(handle, interfaceNumber, static_cast(alternateSetting)) == 0 ? 0 : SCE_USBD_ERROR_OTHER; } std::int32_t APS5_VABI sceUsbdTryLockEvents() { diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index 03a44d4920..fa8edb8b62 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -30,9 +30,17 @@ int UserIndex(int userId) { return -1; } -int ReportAccessibilitySetting(int user_id, int32_t* setting, int32_t value) { - const int index = UserIndex(user_id); +bool IsUserLoggedIn(int userId) { + const int index = UserIndex(userId); if (index < 0) { + return false; + } + std::lock_guard lock(userMutex); + return userConnected[index]; +} + +int ReportAccessibilitySetting(int user_id, int32_t* setting, int32_t value) { + if (!IsUserLoggedIn(user_id)) { return USER_SERVICE_ERROR_NOT_LOGGED_IN; } if (setting == nullptr) { @@ -151,8 +159,7 @@ int APS5_VABI sceUserServiceGetAccessibilityZoomFollowFocus(int user_id, int32_t } int APS5_VABI sceUserServiceGetAgeLevel(int user_id, uint32_t* age_level) { - const int index = UserIndex(user_id); - if (index < 0) { + if (!IsUserLoggedIn(user_id)) { return USER_SERVICE_ERROR_NOT_LOGGED_IN; } if (age_level == nullptr) { diff --git a/core/libs/prx/libSceVdecsw/Export.cpp b/core/libs/prx/libSceVdecsw/Export.cpp index b39ea9bc46..d15ec5c2fa 100644 --- a/core/libs/prx/libSceVdecsw/Export.cpp +++ b/core/libs/prx/libSceVdecsw/Export.cpp @@ -345,11 +345,35 @@ int APS5_VABI sceVdecswSyncDecodeOutput(std::uint64_t handle, OutputInfo* output } int APS5_VABI sceVdecswGetPictureInfo(const OutputInfo* output, void* first, void* second) { - (void)output; - (void)first; - (void)second; - NotImplemented_nid_no_patch(__func__); - return 0; + 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 (first == nullptr) return 0; + std::uint64_t size; + std::memcpy(&size, first, sizeof(size)); + if (size < sizeof(size)) throw std::runtime_error("Vdecsw: picture info is smaller than its size field"); + const auto copy = [first, size](const auto& source) { + const auto bytes = std::min(size, sizeof(source)) - sizeof(size); + std::memcpy(static_cast(first) + sizeof(size), reinterpret_cast(&source) + sizeof(size), bytes); + }; + std::vector> all; + { + std::lock_guard lock(decodersMutex); + for (const auto& [handle, decoder] : decoders) all.push_back(decoder); + } + for (const auto& decoder : all) { + std::lock_guard lock(decoder->mutex); + if (const auto found = decoder->delivered.find(output->frameBuffer); found != decoder->delivered.end()) { + std::visit(copy, found->second); + return 0; + } + if (const auto found = decoder->deliveredVp9.find(output->frameBuffer); found != decoder->deliveredVp9.end()) { + copy(found->second); + return 0; + } + } + throw std::runtime_error("Vdecsw: picture info requested for a frame buffer no decoder returned"); } int APS5_VABI sceVdecswGetAvcPictureInfo(const OutputInfo* output, AvcPictureInfo* first, AvcPictureInfo* second) { diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 5acccb7a32..364cbd74e9 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -149,6 +149,7 @@ if(BUILD_TESTING) target_link_libraries(pad_sdl_tests PRIVATE libSceVideoOut libScePad libSceUserService libc SDL2-static) configure_windows_unwind(pad_sdl_tests) add_test(NAME pad_sdl COMMAND pad_sdl_tests) + set_tests_properties(pad_sdl PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) set_tests_properties(pad_sdl PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$;PATH=path_list_prepend:$;PATH=path_list_prepend:$;PATH=path_list_prepend:$") @@ -291,13 +292,18 @@ if(BUILD_TESTING) endif() endif() -if(BUILD_TESTING AND NOT WIN32) +if(BUILD_TESTING) add_test_executable(video_out_telemetry_tests tests/Telemetry.cpp) target_include_directories(video_out_telemetry_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) target_link_libraries(video_out_telemetry_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc SDL2-static) set_target_properties(video_out_telemetry_tests PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(video_out_telemetry_tests) add_test(NAME video_out_telemetry COMMAND video_out_telemetry_tests) add_test(NAME video_out_telemetry_caption COMMAND video_out_telemetry_tests caption) set_tests_properties(video_out_telemetry PROPERTIES TIMEOUT 30) set_tests_properties(video_out_telemetry_caption PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER}") + if(WIN32) + set_tests_properties(video_out_telemetry video_out_telemetry_caption PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceVideoOut/Unimplemented.cpp b/core/libs/prx/libSceVideoOut/Unimplemented.cpp index 35751e05fd..bd6d217e9f 100644 --- a/core/libs/prx/libSceVideoOut/Unimplemented.cpp +++ b/core/libs/prx/libSceVideoOut/Unimplemented.cpp @@ -38,34 +38,8 @@ int APS5_VABI sceVideoOutCursorSetPosition(int32_t, int32_t, uint32_t, uint32_t) return 0; } -int APS5_VABI sceVideoOutAdjustColor_(int handle, const VideoOutColorSettings* settings, uint32_t settings_size) { - if (settings_size < sizeof(VideoOutColorSettings)) throw std::runtime_error("sceVideoOutAdjustColor_: VIDEO_OUT_ERROR_INVALID_VALUE"); - return sceVideoOutAdjustColor(handle, settings); -} - -int APS5_VABI sceVideoOutColorSettingsSetGamma_(VideoOutColorSettings* settings, float gamma, uint32_t settings_size) { - if (settings_size < sizeof(VideoOutColorSettings)) throw std::runtime_error("sceVideoOutColorSettingsSetGamma_: VIDEO_OUT_ERROR_INVALID_VALUE"); - return sceVideoOutColorSettingsSetGamma(settings, gamma); -} - -int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_(VideoOutColorSpaceConversionSettings* settings, uint32_t settings_size) { - if (settings_size < sizeof(VideoOutColorSpaceConversionSettings)) throw std::runtime_error("sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_: VIDEO_OUT_ERROR_INVALID_VALUE"); - return sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion(settings); -} - -int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_(VideoOutColorSpaceConversionSettings* settings, float lumaScale, uint32_t option, uint32_t settings_size) { - if (settings_size < sizeof(VideoOutColorSpaceConversionSettings)) throw std::runtime_error("sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_: VIDEO_OUT_ERROR_INVALID_VALUE"); - return sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion(settings, lumaScale, option); -} - -int APS5_VABI sceVideoOutAdjustColorSpaceConversion_(int handle, const VideoOutColorSpaceConversionSettings* settings, uint32_t settings_size) { - if (settings_size < sizeof(VideoOutColorSpaceConversionSettings)) throw std::runtime_error("sceVideoOutAdjustColorSpaceConversion_: VIDEO_OUT_ERROR_INVALID_VALUE"); - return sceVideoOutAdjustColorSpaceConversion(handle, settings); -} - int APS5_VABI sceDbgVideoOutAddOutputModeVr60Privilege() { NotImplemented_nid_no_patch(__func__); return 0; } - } diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index 156e03c66d..37fb28e2d7 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -242,6 +243,7 @@ private: void vblankEnd(std::uint64_t tick); void updateDisplayRefreshRate(); void processFlip(FlipRequest& req); + std::optional overlayPlane(); void triggerEvents(VideoOutConfig& cfg, int eventKind, void* triggerData); void pollHostHdr(); diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index f1d49072fb..0ad46a4ed8 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -371,6 +371,31 @@ int APS5_VABI sceVideoOutAdjustColorSpaceConversion(int handle, const VideoOutCo LibcAwaitExit_nid_postfix(); } +int APS5_VABI sceVideoOutAdjustColor_(int handle, const VideoOutColorSettings* settings, uint32_t settings_size) { + if (settings_size < sizeof(VideoOutColorSettings)) throw std::runtime_error("sceVideoOutAdjustColor_: VIDEO_OUT_ERROR_INVALID_VALUE"); + return sceVideoOutAdjustColor(handle, settings); +} + +int APS5_VABI sceVideoOutColorSettingsSetGamma_(VideoOutColorSettings* settings, float gamma, uint32_t settings_size) { + if (settings_size < sizeof(VideoOutColorSettings)) throw std::runtime_error("sceVideoOutColorSettingsSetGamma_: VIDEO_OUT_ERROR_INVALID_VALUE"); + return sceVideoOutColorSettingsSetGamma(settings, gamma); +} + +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_(VideoOutColorSpaceConversionSettings* settings, uint32_t settings_size) { + if (settings_size < sizeof(VideoOutColorSpaceConversionSettings)) throw std::runtime_error("sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_: VIDEO_OUT_ERROR_INVALID_VALUE"); + return sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion(settings); +} + +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_(VideoOutColorSpaceConversionSettings* settings, float lumaScale, uint32_t option, uint32_t settings_size) { + if (settings_size < sizeof(VideoOutColorSpaceConversionSettings)) throw std::runtime_error("sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_: VIDEO_OUT_ERROR_INVALID_VALUE"); + return sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion(settings, lumaScale, option); +} + +int APS5_VABI sceVideoOutAdjustColorSpaceConversion_(int handle, const VideoOutColorSpaceConversionSettings* settings, uint32_t settings_size) { + if (settings_size < sizeof(VideoOutColorSpaceConversionSettings)) throw std::runtime_error("sceVideoOutAdjustColorSpaceConversion_: VIDEO_OUT_ERROR_INVALID_VALUE"); + return sceVideoOutAdjustColorSpaceConversion(handle, settings); +} + int APS5_VABI sceVideoOutSetGlobalBlendSpace(int handle, uint32_t space, uint32_t option) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 377fb3548b..cb8afb2826 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -611,11 +611,12 @@ void VideoOutDriver::processFlip(FlipRequest& req) { request.gpuComplete = true; request.cfg->vblankCond.notify_all(); }; + const auto plane = overlayPlane(); if (req.index >= 0 && !req.unregistered) { const auto display = DescribeVideoOutBuffer(req.buffer, req.group); - AgcDriverPresentBuffer_nid_postfix(target, display, gpuReady, &req); + AgcDriverPresentBuffer_nid_postfix(target, display, plane ? &*plane : nullptr, gpuReady, &req); } else { - AgcDriverPresentClear_nid_postfix(target, req.unregistered || req.index == VIDEO_OUT_BUFFER_INDEX_BLACK, gpuReady, &req); + AgcDriverPresentClear_nid_postfix(target, req.unregistered || req.index == VIDEO_OUT_BUFFER_INDEX_BLACK, plane ? &*plane : nullptr, gpuReady, &req); } timing.Mark("present"); window.UpdateTitle(); @@ -656,6 +657,20 @@ void VideoOutDriver::processFlip(FlipRequest& req) { timing.Mark("notify_game"); } +std::optional VideoOutDriver::overlayPlane() { + std::shared_ptr overlay; + { + std::lock_guard lock(mutex); + overlay = contexts[VIDEO_OUT_BUS_TYPE_OVERLAY + 1]; + } + if (!overlay) return std::nullopt; + std::lock_guard lock(overlay->mutex); + const auto index = overlay->flipStatus.currentBuffer; + if (overlay->Closed() || index < 0 || index >= VIDEO_OUT_BUFFER_NUM_MAX || !overlay->buffers[index].Occupied()) return std::nullopt; + const auto& buffer = overlay->buffers[index]; + return DescribeVideoOutBuffer(buffer, overlay->groups[buffer.groupIndex]); +} + void VideoOutDriver::presentLoop(std::stop_token token, std::promise& started) { try { window.Ensure(VIDEO_OUT_DEFAULT_WIDTH, VIDEO_OUT_DEFAULT_HEIGHT); diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 877e19e1fa..80c15e1fcd 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -2,6 +2,7 @@ #include "prx/libSceVideoOut/include/VideoOutDriver.hpp" #include "prx/libSceVideoOut/include/Buffer.hpp" #include "prx/libSceVideoOut/include/Output.hpp" +#include "prx/libSceVideoOut/include/VideoOut.hpp" #include "prx/libSceVideoOut/include/Event.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" @@ -445,6 +446,22 @@ void testControls() { } VideoOutColorSettings settings{}; expectFailure([&] { sceVideoOutColorSettingsSetGamma(&settings, std::numeric_limits::quiet_NaN()); }); + expectFailure([&] { sceVideoOutColorSettingsSetGamma_(&settings, 1.0f, sizeof(VideoOutColorSettings) - 1); }); + check(sceVideoOutColorSettingsSetGamma_(&settings, 1.2f, sizeof(VideoOutColorSettings)) == 0, "gamma wrapper rejected valid size"); + check(settings.gamma == 1.2f, "gamma wrapper failed to forward value"); + + expectFailure([&] { sceVideoOutAdjustColor_(handle, &settings, sizeof(VideoOutColorSettings) - 1); }); + check(sceVideoOutAdjustColor_(handle, &settings, sizeof(VideoOutColorSettings)) == 0, "adjust color wrapper rejected valid size"); + + VideoOutColorSpaceConversionSettings cscSettings{}; + expectFailure([&] { sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_(&cscSettings, sizeof(VideoOutColorSpaceConversionSettings) - 1); }); + check(sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_(&cscSettings, sizeof(VideoOutColorSpaceConversionSettings)) == 0, "default SDR to HDR wrapper rejected valid size"); + + expectFailure([&] { sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_(&cscSettings, 1.0f, 0, sizeof(VideoOutColorSpaceConversionSettings) - 1); }); + check(sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_(&cscSettings, 1.0f, 0, sizeof(VideoOutColorSpaceConversionSettings)) == 0, "SDR to HDR conversion wrapper rejected valid size"); + + expectFailure([&] { sceVideoOutAdjustColorSpaceConversion_(handle, &cscSettings, sizeof(VideoOutColorSpaceConversionSettings) - 1); }); + check(sceVideoOutAdjustColorSpaceConversion_(handle, &cscSettings, sizeof(VideoOutColorSpaceConversionSettings)) == 0, "adjust color space conversion wrapper rejected valid size"); KernelEqueue queue = 0; check(sceKernelCreateEqueue(&queue, "VideoOut test") == 0, "event queue creation failed"); auto owner = EqueuePin_nid_postfix(queue); @@ -575,36 +592,51 @@ void testOverlay() { sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 259, 137, 0, 0, 0); check(sceVideoOutRegisterBuffers2(mainHandle, 0, 0, &mainBuffer, 1, &attribute, 0, nullptr) == 0, "main buffer registration failed"); check(sceVideoOutRegisterBuffers2(overlayHandle, 0, 0, &overlayBuffer, 1, &attribute, 0, nullptr) == 0, "overlay buffer registration failed"); - const auto flip = [](int handle, const std::shared_ptr& cfg, std::int64_t argument) { + const auto flip = [](int handle, const std::shared_ptr& cfg, std::int64_t argument, int index = 0) { std::uint64_t target; { std::lock_guard lock(cfg->mutex); target = cfg->flipStatus.count + 1; } - check(sceVideoOutSubmitFlip(handle, 0, 1, argument) == 0, "the flip was not accepted"); + check(sceVideoOutSubmitFlip(handle, index, 1, argument) == 0, "the flip was not accepted"); std::unique_lock lock(cfg->mutex); check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return (cfg->failure && cfg->flipStatus.flipPendingNum == 0) || cfg->flipStatus.count == target; }), "the flip did not complete"); if (cfg->failure) std::rethrow_exception(cfg->failure); - check(cfg->flipStatus.flipArg == argument && cfg->flipStatus.currentBuffer == 0 && cfg->flipStatus.flipPendingNum == 0, "flip status is wrong"); + check(cfg->flipStatus.flipArg == argument && cfg->flipStatus.currentBuffer == index && cfg->flipStatus.flipPendingNum == 0, "flip status is wrong"); }; const auto presentsAfter = [](std::uint64_t previous) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); while (AgcDriver::VulkanDevice::PresentCounts().presents == previous && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(10)); return AgcDriver::VulkanDevice::PresentCounts().presents; }; + const auto planes = [] { return AgcDriver::VulkanDevice::PresentCounts().planes; }; const auto start = AgcDriver::VulkanDevice::PresentCounts().presents; + const auto planesAtStart = planes(); flip(mainHandle, mainCfg, 1); const auto afterMain = presentsAfter(start); check(afterMain > start, "a main-bus flip was not presented"); + check(planes() == planesAtStart, "a main-bus flip composited an overlay that never flipped"); flip(overlayHandle, overlayCfg, 2); flip(overlayHandle, overlayCfg, 3); std::this_thread::sleep_for(std::chrono::milliseconds(500)); check(AgcDriver::VulkanDevice::PresentCounts().presents == afterMain, "an overlay-bus flip replaced the main output"); flip(mainHandle, mainCfg, 4); - check(presentsAfter(afterMain) > afterMain, "a main-bus flip after overlay flips was not presented"); + const auto afterOverlay = presentsAfter(afterMain); + check(afterOverlay > afterMain, "a main-bus flip after overlay flips was not presented"); + check(planes() == planesAtStart + 1, "the overlay bus's displayed buffer was not composited over the main output"); + flip(overlayHandle, overlayCfg, 5, VIDEO_OUT_BUFFER_INDEX_BLACK); + flip(mainHandle, mainCfg, 6); + const auto afterBlack = presentsAfter(afterOverlay); + check(afterBlack > afterOverlay && planes() == planesAtStart + 1, "an overlay flipped to black was composited"); + flip(overlayHandle, overlayCfg, 7); sceVideoOutUnregisterBuffers(overlayHandle, 0); - sceVideoOutUnregisterBuffers(mainHandle, 0); + flip(mainHandle, mainCfg, 8); + const auto afterUnregister = presentsAfter(afterBlack); + check(afterUnregister > afterBlack && planes() == planesAtStart + 1, "an unregistered overlay buffer was composited"); sceVideoOutClose(overlayHandle); + flip(mainHandle, mainCfg, 9); + check(presentsAfter(afterUnregister) > afterUnregister && planes() == planesAtStart + 1, "a closed overlay port was composited"); + sceVideoOutUnregisterBuffers(mainHandle, 0); sceVideoOutClose(mainHandle); LibcRunShutdown_nid_postfix(); } diff --git a/core/libs/prx/libSceVideoOut/tests/PadSdl.cpp b/core/libs/prx/libSceVideoOut/tests/PadSdl.cpp index 3bdec619d1..b354fa3c2b 100644 --- a/core/libs/prx/libSceVideoOut/tests/PadSdl.cpp +++ b/core/libs/prx/libSceVideoOut/tests/PadSdl.cpp @@ -29,6 +29,18 @@ int main() { Require(SDL_Init(SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK) == 0); + const int hostJoysticks = SDL_NumJoysticks(); + if (hostJoysticks < 0) { + std::fprintf(stderr, "pad_sdl: SDL_NumJoysticks failed: %s\n", SDL_GetError()); + SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK); + return 1; + } + if (hostJoysticks > 0) { + std::fprintf(stderr, "pad_sdl: skipped, SDL sees %d host joysticks, which take the pad slots this test checks\n", hostJoysticks); + SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK); + return 77; + } + SDL_VirtualJoystickDesc desc{}; desc.version = SDL_VIRTUAL_JOYSTICK_DESC_VERSION; desc.type = SDL_JOYSTICK_TYPE_GAMECONTROLLER; diff --git a/core/libs/prx/libSceVideoOut/tests/Telemetry.cpp b/core/libs/prx/libSceVideoOut/tests/Telemetry.cpp index d3fc3ce183..ba808eb7ba 100644 --- a/core/libs/prx/libSceVideoOut/tests/Telemetry.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Telemetry.cpp @@ -3,7 +3,12 @@ #include #include +#ifdef _WIN32 +#include +#define getpid _getpid +#else #include +#endif #include #include #include @@ -109,7 +114,7 @@ void queueOverflowIsCounted() { void contractPadAndPresentLines() { const auto directory = freshDirectory("contract"); { - auto writer = VideoOutTelemetry::OpenFromEnvironment(directory.c_str(), "abc123"); + auto writer = VideoOutTelemetry::OpenFromEnvironment(directory.string().c_str(), "abc123"); check(writer != nullptr, "variable must open a writer"); PadInputState state; state.buttons = 0x4000; diff --git a/core/libs/prx/libSceVisionManager/Export.cpp b/core/libs/prx/libSceVisionManager/Export.cpp index de5e1986f2..e329980232 100644 --- a/core/libs/prx/libSceVisionManager/Export.cpp +++ b/core/libs/prx/libSceVisionManager/Export.cpp @@ -17,8 +17,7 @@ int APS5_VABI sceVisionManagerCancelRequest() { } int APS5_VABI sceVisionManagerClose() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80e70004); } int APS5_VABI sceVisionManagerFinalize() { @@ -107,23 +106,19 @@ int APS5_VABI sceVisionManagerGetResultOfFindPadCorrespondRegion() { } int APS5_VABI sceVisionManagerGetResultOfFindUser() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80e70004); } int APS5_VABI sceVisionManagerGetResultOfFindUserInRegion() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80e70004); } int APS5_VABI sceVisionManagerGetResultOfFindUserWithoutIdentify() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80e70004); } int APS5_VABI sceVisionManagerGetResultOfRegisterUserInRegion() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80e70004); } int APS5_VABI sceVisionManagerGetStoreCameraImgYPtr() { @@ -142,13 +137,11 @@ int APS5_VABI sceVisionManagerInitialize() { } int APS5_VABI sceVisionManagerLoadSettings() { - NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceVisionManagerOpen() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80e70004); } int APS5_VABI sceVisionManagerRegistryClear() { @@ -192,18 +185,15 @@ int APS5_VABI sceVisionManagerRequestFindPadCorrespondRegion() { } int APS5_VABI sceVisionManagerRequestFindUser() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80e70004); } int APS5_VABI sceVisionManagerRequestFindUserInRegion() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80e70004); } int APS5_VABI sceVisionManagerRequestFindUserWithoutIdentify() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80e70004); } int APS5_VABI sceVisionManagerRequestHeadDetection() { @@ -232,8 +222,7 @@ int APS5_VABI sceVisionManagerRequestRecogUtility() { } int APS5_VABI sceVisionManagerRequestRegisterUserInRegion() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80e70004); } int APS5_VABI sceVisionManagerRequestStoreCameraImgY() { diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index a7dfe71792..18c75c1db4 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -447,7 +447,7 @@ int APS5_VABI sceVoiceQAEnableRecording(void) { return 0; } -int APS5_VABI sceVoiceVADAdjustment(void) { +int APS5_VABI sceVoiceVADAdjustment(float, float) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceVoice/include/Api.hpp b/core/libs/prx/libSceVoice/include/Api.hpp index cdaf1823b6..4ac2972a27 100644 --- a/core/libs/prx/libSceVoice/include/Api.hpp +++ b/core/libs/prx/libSceVoice/include/Api.hpp @@ -32,7 +32,7 @@ int APS5_VABI sceVoiceInitHQ(const VoiceInitParam* param, std::int32_t version); int APS5_VABI sceVoiceGetMemorySize(void); int APS5_VABI sceVoiceDeviceInCustomerData(void); int APS5_VABI sceVoiceQAEnableRecording(void); -int APS5_VABI sceVoiceVADAdjustment(void); +int APS5_VABI sceVoiceVADAdjustment(float param1, float param2); int APS5_VABI sceVoiceVADPVTAdjustment(void); int APS5_VABI sceVoiceVADSetVersion(std::int32_t version); int APS5_VABI sceVoiceSetMuteFlagAll(bool muted); diff --git a/core/libs/prx/libSceVoiceChat/Export.cpp b/core/libs/prx/libSceVoiceChat/Export.cpp index b1fde4c767..9f3c11f943 100644 --- a/core/libs/prx/libSceVoiceChat/Export.cpp +++ b/core/libs/prx/libSceVoiceChat/Export.cpp @@ -31,8 +31,8 @@ extern "C" { int APS5_VABI sceVoiceChatInitialize(void* param) { (void)param; return 0; } int APS5_VABI sceVoiceChatTerminate(void) { return 0; } -int APS5_VABI sceVoiceChatRegisterHandlers(void* a) { (void)a; return 0; } -int APS5_VABI sceVoiceChatRegisterMicEventHandler(void* a, void* b) { (void)a; (void)b; return 0; } +int APS5_VABI sceVoiceChatRegisterHandlers(void* handlers, void* userArg) { (void)handlers; (void)userArg; return 0; } +int APS5_VABI sceVoiceChatRegisterMicEventHandler(std::int32_t userId, void* handler, void* userArg) { (void)userId; (void)handler; (void)userArg; return 0; } int APS5_VABI sceVoiceChatProcessEvent(void) { return 0; } int APS5_VABI sceVoiceChatDeleteRequest(int requestId) { (void)requestId; return 0; } diff --git a/core/libs/prx/libSceVoiceChat/include/Api.hpp b/core/libs/prx/libSceVoiceChat/include/Api.hpp index 5d5676dff3..3b3df314de 100644 --- a/core/libs/prx/libSceVoiceChat/include/Api.hpp +++ b/core/libs/prx/libSceVoiceChat/include/Api.hpp @@ -6,8 +6,8 @@ extern "C" { int APS5_VABI sceVoiceChatInitialize(void* param); int APS5_VABI sceVoiceChatTerminate(void); -int APS5_VABI sceVoiceChatRegisterHandlers(void* a); -int APS5_VABI sceVoiceChatRegisterMicEventHandler(void* a, void* b); +int APS5_VABI sceVoiceChatRegisterHandlers(void* handlers, void* userArg); +int APS5_VABI sceVoiceChatRegisterMicEventHandler(std::int32_t userId, void* handler, void* userArg); int APS5_VABI sceVoiceChatProcessEvent(void); int APS5_VABI sceVoiceChatDeleteRequest(int requestId); int APS5_VABI sceVoiceChatCreateRequest(void* a, void* b); diff --git a/core/libs/prx/libSceVrTracker2/CMakeLists.txt b/core/libs/prx/libSceVrTracker2/CMakeLists.txt index aaf8e867d1..cccd8e8455 100644 --- a/core/libs/prx/libSceVrTracker2/CMakeLists.txt +++ b/core/libs/prx/libSceVrTracker2/CMakeLists.txt @@ -1,12 +1,12 @@ add_library(libSceVrTracker2 SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceVrTracker2 PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(libSceVrTracker2 PRIVATE libc) +target_link_libraries(libSceVrTracker2 PRIVATE libSceHmd2 libc) configure_windows_unwind(libSceVrTracker2) if(BUILD_TESTING) add_test_executable(guest_vr_tracker2_tests ${LIBS_INCLUDE_DIR}/tests/GuestVrTracker2.cpp) target_include_directories(guest_vr_tracker2_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_vr_tracker2_tests PRIVATE libSceVrTracker2 libc) + target_link_libraries(guest_vr_tracker2_tests PRIVATE libSceVrTracker2 libkernel libc) configure_windows_unwind(guest_vr_tracker2_tests) add_test(NAME guest_vr_tracker2 COMMAND guest_vr_tracker2_tests) if(WIN32) diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index 680bd23351..f9246da310 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -1,4 +1,6 @@ #include "prx/libSceVrTracker2/include/Api.hpp" +#include "prx/libSceHmd2/libSceHmd2.hpp" +#include #include #include #include @@ -48,8 +50,96 @@ namespace { constexpr std::int32_t SCE_OK = 0; constexpr std::int32_t VR_TRACKER2_ERROR_NOT_INITIALIZED = static_cast(0x8A740001u); +constexpr std::int32_t VR_TRACKER2_ERROR_ALREADY_INITIALIZED = static_cast(0x8A740002u); constexpr std::int32_t VR_TRACKER2_ERROR_INVALID_ARGUMENT = static_cast(0x8A740006u); +constexpr std::uint32_t TrackerMode = 0; +constexpr std::uint32_t SystemTrackingMode = 1000; +constexpr std::uint32_t SystemClientMode = 1001; +constexpr std::uint32_t MinimumWorkMemoryAlignment = 0x10000; +constexpr std::int32_t MinimumThreadPriority = 256; +constexpr std::int32_t MaximumThreadPriority = 767; +constexpr std::uint64_t TrackerAffinityLimit = 0x2000; +constexpr std::uint64_t SystemAffinityMask = 0xEA00; +constexpr std::uint32_t OptionModeSdkVersion = 0x7000000; + +std::atomic state{0}; + +std::uint32_t WorkMemorySize(std::uint32_t mode) { + if (mode == SystemTrackingMode) return 7u << 20; + if (mode == SystemClientMode) return 3u << 20; + return 32u << 20; +} + +std::uint32_t CompiledSdkVersion() { + const auto* process = static_cast(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t sdkVersion; + std::memcpy(&sdkVersion, process + 0x10, sizeof(sdkVersion)); + return sdkVersion; +} + +std::int32_t ValidateMode(std::uint32_t mode) { + if (mode == SystemTrackingMode || mode == SystemClientMode) NotImplemented_nid_no_patch("sceVrTracker2Initialize: application category of a system mode"); + return mode == TrackerMode ? SCE_OK : VR_TRACKER2_ERROR_INVALID_ARGUMENT; +} + +bool PriorityAllowed(std::int32_t priority) { + return priority >= MinimumThreadPriority && priority <= MaximumThreadPriority; +} + +bool AffinityAllowed(std::uint32_t mode, std::uint64_t mask) { + if (mode == TrackerMode) return mask < TrackerAffinityLimit; + return (mask & ~SystemAffinityMask) == 0; +} + +bool OptionModeAllowed(std::int8_t optionMode) { + if (optionMode == -1 || optionMode == 0) return true; + return optionMode == 1 && CompiledSdkVersion() >= OptionModeSdkVersion; +} + +class InitializeSession { +public: + std::int32_t OpenTracking() { + return track(sceHmd2InternalTrackingOpen(), trackingOpened); + } + + std::int32_t StartTracking() { + return track(sceHmd2InternalTrackingStart(), trackingStarted); + } + + std::int32_t OpenLedDetector() { + return track(sceHmd2InternalLedDetectorOpen(), ledDetectorOpened); + } + + std::int32_t StartLedDetector() { + return track(sceHmd2InternalLedDetectorStart(), ledDetectorStarted); + } + + void Rollback() { + if (ledDetectorStarted) sceHmd2InternalLedDetectorStop(); + if (ledDetectorOpened) sceHmd2InternalLedDetectorClose(); + if (trackingStarted) sceHmd2InternalTrackingStop(); + if (trackingOpened) sceHmd2InternalTrackingClose(); + state.store(0); + } + + std::int32_t Abort(std::int32_t result) { + Rollback(); + return result; + } + +private: + static std::int32_t track(std::int32_t result, bool& done) { + done = result == SCE_OK; + return result; + } + + bool trackingOpened = false; + bool trackingStarted = false; + bool ledDetectorOpened = false; + bool ledDetectorStarted = false; +}; + std::int32_t ClearThenReportNotInitialized(void* output, std::size_t size) { if (output == nullptr) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; std::memset(output, 0, size); @@ -127,16 +217,12 @@ std::int32_t APS5_VABI sceVrTracker2UnregisterDevice(std::int32_t deviceHandle) std::int32_t APS5_VABI sceVrTracker2QueryMemory(const VrTracker2MemoryParameter* parameter, VrTracker2MemoryRequirement* requirement) { if (parameter == nullptr || requirement == nullptr) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; if (parameter->size != sizeof(VrTracker2MemoryParameter)) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; - std::uint32_t memorySize = 32u << 20; - if (parameter->mode == 1000 || parameter->mode == 1001) { - const auto* process = static_cast(ApplicationProcessParameters_nid_no_patch()); - std::uint32_t sdkVersion; - std::memcpy(&sdkVersion, process + 0x10, sizeof(sdkVersion)); - if (sdkVersion < 0x1000000) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; - memorySize = parameter->mode == 1000 ? 7u << 20 : 3u << 20; - } else if (parameter->mode != 0) { + if (parameter->mode == SystemTrackingMode || parameter->mode == SystemClientMode) { + if (CompiledSdkVersion() < 0x1000000) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + } else if (parameter->mode != TrackerMode) { return VR_TRACKER2_ERROR_INVALID_ARGUMENT; } + const std::uint32_t memorySize = WorkMemorySize(parameter->mode); for (const auto value : parameter->reserved) if (value != 0) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; *requirement = {sizeof(VrTracker2MemoryRequirement), memorySize, 64u << 10, {}}; @@ -144,6 +230,30 @@ std::int32_t APS5_VABI sceVrTracker2QueryMemory(const VrTracker2MemoryParameter* } std::int32_t APS5_VABI sceVrTracker2Initialize(const VrTracker2InitializeParameter* parameter) { + if (parameter == nullptr || parameter->size != sizeof(VrTracker2InitializeParameter)) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + const std::uint32_t mode = parameter->mode; + if (const std::int32_t result = ValidateMode(mode); result != SCE_OK) return result; + if (parameter->workMemory == nullptr) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + if (parameter->workMemorySize < WorkMemorySize(mode)) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + if (parameter->workMemoryAlignment < MinimumWorkMemoryAlignment) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + for (const auto value : parameter->reserved) + if (value != 0) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + for (const auto priority : parameter->threadPriorities) + if (!PriorityAllowed(priority)) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + for (const auto mask : parameter->threadAffinityMasks) + if (!AffinityAllowed(mode, mask)) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + std::uint32_t expected = 0; + if (!state.compare_exchange_strong(expected, 1)) return VR_TRACKER2_ERROR_ALREADY_INITIALIZED; + InitializeSession session; + if (mode == TrackerMode) std::memset(parameter->workMemory, 0, WorkMemorySize(mode)); + if (mode != SystemClientMode) { + if (!OptionModeAllowed(parameter->optionMode)) return session.Abort(VR_TRACKER2_ERROR_INVALID_ARGUMENT); + if (const std::int32_t result = session.OpenTracking(); result != SCE_OK) return session.Abort(result); + if (const std::int32_t result = session.StartTracking(); result != SCE_OK) return session.Abort(result); + if (const std::int32_t result = session.OpenLedDetector(); result != SCE_OK) return session.Abort(result); + if (const std::int32_t result = session.StartLedDetector(); result != SCE_OK) return session.Abort(result); + } + session.Rollback(); NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 0ca57aa2a7..2fb827e104 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -172,12 +172,14 @@ if(WIN32) endif() if(BUILD_TESTING) - if(UNIX) - add_test_executable(guest_fopen_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestFopenErrors.cpp) - target_include_directories(guest_fopen_errors_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_fopen_errors_tests PRIVATE libc) - add_test(NAME guest_fopen_errors COMMAND guest_fopen_errors_tests) - set_tests_properties(guest_fopen_errors PROPERTIES TIMEOUT 20) + add_test_executable(guest_fopen_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestFopenErrors.cpp) + target_include_directories(guest_fopen_errors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_fopen_errors_tests PRIVATE libc) + add_test(NAME guest_fopen_errors COMMAND guest_fopen_errors_tests) + set_tests_properties(guest_fopen_errors PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(guest_fopen_errors PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() add_test_executable(guest_index_tests ${LIBS_INCLUDE_DIR}/tests/GuestIndex.cpp) @@ -638,6 +640,15 @@ if(BUILD_TESTING) set_tests_properties(guest_memrchr PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_string_compare_tests ${LIBS_INCLUDE_DIR}/tests/GuestStringCompare.cpp) + target_include_directories(guest_string_compare_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_string_compare_tests PRIVATE libc) + configure_windows_unwind(guest_string_compare_tests) + add_test(NAME guest_string_compare COMMAND guest_string_compare_tests) + if(WIN32) + set_tests_properties(guest_string_compare PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_daemon_tests ${LIBS_INCLUDE_DIR}/tests/GuestDaemon.cpp) target_include_directories(guest_daemon_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_daemon_tests PRIVATE libc) @@ -920,6 +931,24 @@ if(BUILD_TESTING) set_tests_properties(guest_bad_optional_access PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_underflow_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestUnderflowError.cpp) + target_include_directories(guest_underflow_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_underflow_error_tests PRIVATE libc) + configure_windows_unwind(guest_underflow_error_tests) + add_test(NAME guest_underflow_error COMMAND guest_underflow_error_tests) + if(WIN32) + set_tests_properties(guest_underflow_error PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + if(WIN32) + add_test_executable(exception_personality_tests ${LIBS_INCLUDE_DIR}/tests/ExceptionPersonality.cpp) + target_include_directories(exception_personality_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(exception_personality_tests PRIVATE libc) + configure_windows_unwind(exception_personality_tests) + add_test(NAME exception_personality COMMAND exception_personality_tests) + set_tests_properties(exception_personality PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() if(UNIX) add_test_executable(exception_personality_tests ${LIBS_INCLUDE_DIR}/tests/ExceptionPersonality.cpp) target_include_directories(exception_personality_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index afdb17d63c..4373c3455a 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -163,6 +163,15 @@ int APS5_VABI _Atomic_compare_exchange_strong_8_nid_postfix(volatile std::uint64 int APS5_VABI _Atomic_compare_exchange_weak_1_nid_postfix(volatile std::uint8_t* target, std::uint8_t* expected, std::uint8_t desired, int, int); int APS5_VABI _Atomic_compare_exchange_weak_2_nid_postfix(volatile std::uint16_t* target, std::uint16_t* expected, std::uint16_t desired, int, int); int APS5_VABI _Atomic_compare_exchange_weak_8_nid_postfix(volatile std::uint64_t* target, std::uint64_t* expected, std::uint64_t desired, int, int); +std::uint8_t APS5_VABI _Fetch_and_seq_cst_1_nid_postfix(volatile std::uint8_t* target, std::uint8_t value); +std::uint16_t APS5_VABI _Fetch_and_seq_cst_2_nid_postfix(volatile std::uint16_t* target, std::uint16_t value); +std::uint32_t APS5_VABI _Fetch_and_seq_cst_4_nid_postfix(volatile std::uint32_t* target, std::uint32_t value); +std::uint8_t APS5_VABI _Fetch_or_seq_cst_1_nid_postfix(volatile std::uint8_t* target, std::uint8_t value); +std::uint16_t APS5_VABI _Fetch_or_seq_cst_2_nid_postfix(volatile std::uint16_t* target, std::uint16_t value); +std::uint32_t APS5_VABI _Fetch_or_seq_cst_4_nid_postfix(volatile std::uint32_t* target, std::uint32_t value); +std::uint8_t APS5_VABI _Fetch_xor_seq_cst_1_nid_postfix(volatile std::uint8_t* target, std::uint8_t value); +std::uint16_t APS5_VABI _Fetch_xor_seq_cst_2_nid_postfix(volatile std::uint16_t* target, std::uint16_t value); +std::uint32_t APS5_VABI _Fetch_xor_seq_cst_4_nid_postfix(volatile std::uint32_t* target, std::uint32_t value); std::uint8_t APS5_VABI _Atomic_fetch_add_1_nid_postfix(volatile std::uint8_t* target, std::uint8_t value, int); std::uint16_t APS5_VABI _Atomic_fetch_add_2_nid_postfix(volatile std::uint16_t* target, std::uint16_t value, int); std::uint64_t APS5_VABI _Atomic_fetch_add_8_nid_postfix(volatile std::uint64_t* target, std::uint64_t value, int order); @@ -200,6 +209,9 @@ float APS5_VABI _FSin_nid_postfix(float x, unsigned int quadrant); long double APS5_VABI _LSin_nid_postfix(long double x, unsigned int quadrant); double APS5_VABI _Log_nid_postfix(double x, int base); float APS5_VABI _FLog_nid_postfix(float x, int base); +long double APS5_VABI _LLog_nid_postfix(long double x, int base); +long double APS5_VABI _LDivide_nid_postfix(long double x, long double y); +long double APS5_VABI _LRecip_nid_postfix(long double x); void* APS5_VABI opendir_nid_postfix(const char* path); GuestDirectoryEntry* APS5_VABI readdir_nid_postfix(void* handle); int APS5_VABI readdir_r_nid_postfix(void* handle, GuestDirectoryEntry* entry, GuestDirectoryEntry** result); @@ -216,6 +228,7 @@ int APS5_VABI strerror_r_nid_postfix(int error, char* buffer, std::size_t length void APS5_VABI perror_nid_postfix(const char* prefix); FileStream* APS5_VABI fdopen_nid_postfix(int descriptor, const char* mode); FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode, FileStream* stream); +int APS5_VABI freopen_s_nid_postfix(FileStream** result, const char* filename, const char* mode, FileStream* stream); FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode); int APS5_VABI fopen_s_nid_postfix(FileStream** result, const char* filename, const char* mode); int APS5_VABI fclose_nid_postfix(FileStream* stream); @@ -352,6 +365,13 @@ int APS5_VABI puts_nid_postfix(const char* s); int APS5_VABI vfscanf_nid_postfix(FileStream* stream, const char* format, VaList* args); int APS5_VABI vscanf_nid_postfix(const char* format, VaList* args); int APS5_VABI scanf_nid_postfix(const char* format, ...); +int APS5_VABI vfprintf_s_nid_postfix(FileStream* stream, const char* format, VaList* args); +int APS5_VABI vprintf_s_nid_postfix(const char* format, VaList* args); +int APS5_VABI fprintf_s_nid_postfix(FileStream* stream, const char* format, ...); +int APS5_VABI vfscanf_s_nid_postfix(FileStream* stream, const char* format, VaList* args); +int APS5_VABI vscanf_s_nid_postfix(const char* format, VaList* args); +int APS5_VABI fscanf_s_nid_postfix(FileStream* stream, const char* format, ...); +int APS5_VABI scanf_s_nid_postfix(const char* format, ...); int APS5_VABI vswprintf_nid_postfix(char16_t* buffer, std::size_t size, const char16_t* format, VaList* args); int APS5_VABI swprintf_nid_postfix(char16_t* buffer, std::size_t size, const char16_t* format, ...); int APS5_VABI snwprintf_s_nid_postfix(char16_t* buffer, std::size_t size, const char16_t* format, ...); @@ -361,6 +381,10 @@ int APS5_VABI vsnwprintf_s_nid_postfix(char16_t* buffer, std::size_t size, const int APS5_VABI wprintf_nid_postfix(const char16_t* format, ...); int APS5_VABI vfwprintf_nid_postfix(FileStream* stream, const char16_t* format, VaList* args); int APS5_VABI fwprintf_nid_postfix(FileStream* stream, const char16_t* format, ...); +int APS5_VABI vfwprintf_s_nid_postfix(FileStream* stream, const char16_t* format, VaList* args); +int APS5_VABI vwprintf_s_nid_postfix(const char16_t* format, VaList* args); +int APS5_VABI fwprintf_s_nid_postfix(FileStream* stream, const char16_t* format, ...); +int APS5_VABI wprintf_s_nid_postfix(const char16_t* format, ...); int APS5_VABI vwprintf_nid_postfix(const char16_t* format, VaList* args); int APS5_VABI fputwc_nid_postfix(char16_t value, FileStream* stream); int APS5_VABI fputws_nid_postfix(const char16_t* str, FileStream* stream); @@ -601,6 +625,8 @@ float APS5_VABI __powisf2_nid_postfix(float base, int exponent); double APS5_VABI __powidf2_nid_postfix(double base, int exponent); float APS5_VABI expf_nid_postfix(float x); float APS5_VABI exp2f_nid_postfix(float x); +short APS5_VABI _Exp_nid_postfix(double* value, double y, short offset); +short APS5_VABI _FExp_nid_postfix(float* value, float y, short offset); float APS5_VABI logf_nid_postfix(float x); float APS5_VABI log2f_nid_postfix(float x); double APS5_VABI log10_nid_postfix(double x); @@ -610,6 +636,10 @@ float APS5_VABI roundf_nid_postfix(float x); double APS5_VABI round_nid_postfix(double x); float APS5_VABI cbrtf_nid_postfix(float x); float APS5_VABI remainderf_nid_postfix(float x, float y); +double APS5_VABI _Divide_nid_postfix(double x, double y); +float APS5_VABI _FDivide_nid_postfix(float x, float y); +double APS5_VABI _Recip_nid_postfix(double x); +float APS5_VABI _FRecip_nid_postfix(float x); float APS5_VABI nextafterf_nid_postfix(float x, float y); double APS5_VABI nextafter_nid_postfix(double x, double y); int APS5_VABI __isfinite_nid_postfix(double x); @@ -662,6 +692,8 @@ void APS5_VABI srand_nid_postfix(unsigned int seed); int APS5_VABI __fpclassifyd_nid_postfix(double value); double APS5_VABI _Sinh_nid_postfix(double x, double y); double APS5_VABI _Cosh_nid_postfix(double x, double y); +long double APS5_VABI _LCosh_nid_postfix(long double x, long double y); +long double APS5_VABI _LSinh_nid_postfix(long double x, long double y); int APS5_VABI _Fltrounds_nid_postfix(void); int APS5_VABI fegetenv_nid_postfix(GuestFloatingPointEnvironment* environment); long double APS5_VABI copysignl_nid_postfix(long double x, long double y); @@ -779,6 +811,7 @@ int APS5_VABI iswpunct_nid_postfix(std::uint32_t c); int APS5_VABI iswspace_nid_postfix(std::uint32_t c); int APS5_VABI iswupper_nid_postfix(std::uint32_t c); int APS5_VABI iswxdigit_nid_postfix(std::uint32_t c); +int APS5_VABI iswascii_nid_postfix(std::uint32_t c); int APS5_VABI iswctype_nid_postfix(std::uint32_t c, std::uint64_t mask); int APS5_VABI _Iswctype_nid_postfix(std::uint32_t c, std::uint64_t type); std::uint64_t APS5_VABI wctype_nid_postfix(const char* property); @@ -987,6 +1020,7 @@ int APS5_VABI _Mtx_init_with_name_nid_postfix(void** handle, int type, const cha void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); int APS5_VABI _Mtx_lock_nid_postfix(void** handle); int APS5_VABI _Mtx_trylock_nid_postfix(void** handle); +int APS5_VABI _Mtx_timedlock_nid_postfix(void** handle, const Xtime* target); int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); int APS5_VABI _Cnd_init_nid_postfix(void** handle); int APS5_VABI _Cnd_init_with_default_name_override_nid_postfix(void** handle, const char*); @@ -1002,9 +1036,10 @@ void APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(GuestPad* pad); void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(GuestPad* pad); void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad); void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, void** thread); +void APS5_VABI _ZNSt4_Pad7_LaunchEPP12pthread_attrPP7pthread_nid_postfix(GuestPad* pad, void** attrSlot, void** thread); int APS5_VABI _Thrd_join_nid_postfix(void* thread, int* code); int APS5_VABI _Thrd_detach_nid_postfix(void* thread); -int APS5_VABI _Thrd_id_nid_postfix(void); +void* APS5_VABI _Thrd_id_nid_postfix(void); void APS5_VABI _Call_once_nid_postfix(std::int32_t* flag, void (APS5_VABI* function)(void)); int APS5_VABI _Tss_create_nid_postfix(int* key, void (APS5_VABI* destructor)(void*)); int APS5_VABI _Tss_delete_nid_postfix(int key); @@ -1258,6 +1293,7 @@ void APS5_VABI _ZNSt11range_errorD1Ev_nid_postfix(LibcException::ExceptionObject void APS5_VABI _ZNSt11range_errorD2Ev_nid_postfix(LibcException::ExceptionObject* self); void APS5_VABI _ZNSt11range_errorD0Ev_nid_postfix(LibcException::ExceptionObject* self); void APS5_VABI _ZNSt14overflow_errorD1Ev_nid_postfix(LibcException::ExceptionObject* self); +void APS5_VABI _ZNSt15underflow_errorD1Ev_nid_postfix(LibcException::ExceptionObject* self); void APS5_VABI _ZNSt8ios_base7failureD1Ev_nid_postfix(LibcException::ExceptionObject* self); void APS5_VABI _ZNSt8ios_base7failureD2Ev_nid_postfix(LibcException::ExceptionObject* self); void APS5_VABI _ZNSt8ios_base7failureD0Ev_nid_postfix(LibcException::ExceptionObject* self); diff --git a/core/libs/prx/libc/include/AppArchive.hpp b/core/libs/prx/libc/include/AppArchive.hpp index 13d25f34c3..23a1497885 100644 --- a/core/libs/prx/libc/include/AppArchive.hpp +++ b/core/libs/prx/libc/include/AppArchive.hpp @@ -21,6 +21,8 @@ struct AppArchiveBuffer { struct AppArchiveDirectoryEntry { std::string Name; bool Directory; + std::uint32_t Handle; + std::uint64_t Size; }; extern "C" { @@ -28,6 +30,9 @@ extern "C" { bool AppArchiveLookup_nid_no_patch(const std::filesystem::path& hostPath, AppArchiveNode* node); std::uint64_t AppArchiveRead_nid_no_patch(std::uint32_t handle, std::uint64_t offset, void* buffer, std::uint64_t length); std::vector AppArchiveList_nid_no_patch(std::uint32_t handle); +std::vector AppArchiveListDirectory_nid_no_patch(const std::filesystem::path& hostPath); +std::vector AppArchiveListUnshadowedDirectory_nid_no_patch(const std::filesystem::path& hostPath, const std::vector& hostNames); +bool AppArchiveNameMatches_nid_no_patch(const std::string& left, const std::string& right); void AppArchiveReady_nid_no_patch(); void AppArchiveRefuseOnly_nid_no_patch(const std::filesystem::path& hostPath, const char* guestPath, const char* operation); void AppArchiveRefuseMergedListing_nid_no_patch(const std::filesystem::path& hostPath, const char* guestPath); diff --git a/core/libs/prx/libc/include/CallTrace.hpp b/core/libs/prx/libc/include/CallTrace.hpp index b53c1ba96e..430700f0b5 100644 --- a/core/libs/prx/libc/include/CallTrace.hpp +++ b/core/libs/prx/libc/include/CallTrace.hpp @@ -23,6 +23,7 @@ void* TraceImport_nid_no_patch(void* target, const char* name, const char* libra std::size_t CopyCallTrace_nid_no_patch(CallTraceEntry* entries, std::size_t capacity); std::size_t CopyThreadCallTrace_nid_no_patch(std::uint64_t thread, CallTraceEntry* entries, std::size_t capacity); std::size_t CopyCallCounts_nid_no_patch(CallTraceCount* entries, std::size_t capacity, std::uint64_t* total); +bool WriteCallCounts_nid_no_patch(const char* path); } #endif diff --git a/core/libs/prx/libc/include/ChildProcesses.hpp b/core/libs/prx/libc/include/ChildProcesses.hpp new file mode 100644 index 0000000000..9554bfb475 --- /dev/null +++ b/core/libs/prx/libc/include/ChildProcesses.hpp @@ -0,0 +1,11 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_CHILDPROCESSES_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_CHILDPROCESSES_HPP + +#ifdef _WIN32 +#include + +extern "C" void RegisterChildProcess_nid_no_patch(std::int32_t processId, std::int64_t processHandle); +extern "C" std::int64_t WaitChildProcess_nid_no_patch(std::int32_t processId, unsigned options, int* status); +#endif + +#endif diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 73140da0c7..a97f626f49 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -234,6 +234,9 @@ public: #ifdef _WIN32 const bool endOfFile = std::feof(GetHandle()) != 0; const bool error = std::ferror(GetHandle()) != 0; +#elif defined(__APPLE__) + const bool endOfFile = feof_unlocked(GetHandle()) != 0; + const bool error = ferror_unlocked(GetHandle()) != 0; #else const bool endOfFile = feof_unlocked(GetHandle()) != 0; const bool error = ferror_unlocked(GetHandle()) != 0; diff --git a/core/libs/prx/libc/include/FilesystemError.hpp b/core/libs/prx/libc/include/FilesystemError.hpp index bb50c8d70e..b3b6b2aca3 100644 --- a/core/libs/prx/libc/include/FilesystemError.hpp +++ b/core/libs/prx/libc/include/FilesystemError.hpp @@ -39,6 +39,8 @@ inline int NotFoundError_nid_no_patch(const std::filesystem::path& native) { std::error_code RenameNative_nid_no_patch(const std::filesystem::path& source, const std::filesystem::path& destination, bool replacesExisting); +extern "C" bool ExecutePermission_nid_no_patch(const std::filesystem::path& native); + inline int ResolvePathError_nid_no_patch(const char* path, std::filesystem::path& resolved, bool module = false, bool archived = false) { try { resolved = archived ? ResolveArchivedPath_nid_no_patch(path, module) : module ? ResolvePathOrRelinkedModule_nid_no_patch(path) : ResolvePath_nid_no_patch(path); diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index b45386ab48..7596a572a2 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -27,8 +27,11 @@ extern "C" std::FILE* HostStandardErrorFile_nid_no_patch(); #endif extern "C" bool CopyGuestEnvironment_nid_no_patch(const char* name, std::string& value); +extern "C" int CopyGuestString_nid_no_patch(const char* source, std::size_t maximum, std::string& destination); +extern "C" int CopyGuestPath_nid_no_patch(const char* source, std::string& destination); extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path); +extern "C" bool GuestCopyOut_nid_no_patch(void* destination, const void* source, std::size_t bytes); extern "C" std::filesystem::path ResolvePathOrRelinkedModule_nid_no_patch(const char* path); extern "C" std::filesystem::path ResolveArchivedPath_nid_no_patch(const char* path, bool module = false); extern "C" std::filesystem::path ResolveDiskPath_nid_no_patch(const char* path); diff --git a/core/libs/prx/libc/include/GuestDirectory.hpp b/core/libs/prx/libc/include/GuestDirectory.hpp index 3f83ab1a18..775ca51c4f 100644 --- a/core/libs/prx/libc/include/GuestDirectory.hpp +++ b/core/libs/prx/libc/include/GuestDirectory.hpp @@ -14,4 +14,5 @@ struct GuestDirectoryEntry { static_assert(offsetof(GuestDirectoryEntry, name) == 8); static_assert(sizeof(GuestDirectoryEntry) == 264); -extern "C" void* APS5_VABI OpenDescriptorDirectory_nid_no_patch(const char* hostPath, int descriptor, int (APS5_VABI* closeDescriptor)(int), std::uint64_t position); +extern "C" void* APS5_VABI OpenDescriptorDirectory_nid_no_patch(const char* hostPath, int descriptor, int (APS5_VABI* closeDescriptor)(int), std::uint64_t position, + int (APS5_VABI* readDescriptor)(int, char*, int), std::int64_t (APS5_VABI* seekDescriptor)(int, std::int64_t, int)); diff --git a/core/libs/prx/libc/include/GuestFileNumber.hpp b/core/libs/prx/libc/include/GuestFileNumber.hpp new file mode 100644 index 0000000000..58512f9b14 --- /dev/null +++ b/core/libs/prx/libc/include/GuestFileNumber.hpp @@ -0,0 +1,11 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_GUESTFILENUMBER_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_GUESTFILENUMBER_HPP + +#include + +inline constexpr std::uint32_t GuestFileNumber_nid_no_patch(std::uint64_t identity) noexcept { + const auto folded = static_cast(identity) ^ (static_cast(identity >> 32) * 0x9E3779B1u); + return folded == 0 ? 1 : folded; +} + +#endif diff --git a/core/libs/prx/libc/include/JumpBuffer.hpp b/core/libs/prx/libc/include/JumpBuffer.hpp new file mode 100644 index 0000000000..07e272b61e --- /dev/null +++ b/core/libs/prx/libc/include/JumpBuffer.hpp @@ -0,0 +1,18 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_JUMPBUFFER_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_JUMPBUFFER_HPP + +#include "prx/libc/include/general/VabiMacros.hpp" + +namespace JumpBuffer { + +using Sigprocmask = int (APS5_VABI*)(int how, const void* set, void* previousSet); + +extern "C" { + +void GuestSigprocmaskRegister_nid_no_patch(Sigprocmask sigprocmask); + +} + +} + +#endif diff --git a/core/libs/prx/libc/include/SonyCrt.hpp b/core/libs/prx/libc/include/SonyCrt.hpp index db53cd3d54..fc97c753f1 100644 --- a/core/libs/prx/libc/include/SonyCrt.hpp +++ b/core/libs/prx/libc/include/SonyCrt.hpp @@ -88,7 +88,7 @@ inline int SonyFormatAsctime(char* buffer, std::size_t size, const GuestTm& time static const char* const days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; static const char* const months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; if (!buffer || size < 26) return EINVAL; - const int year = 1900 + time.tm_year; + const std::int64_t year = std::int64_t{time.tm_year} + 1900; if (time.tm_wday < 0 || time.tm_wday > 6 || time.tm_mon < 0 || time.tm_mon > 11 || time.tm_mday < 1 || time.tm_mday > 31 || time.tm_hour < 0 || time.tm_hour > 23 || time.tm_min < 0 || time.tm_min > 59 || time.tm_sec < 0 || time.tm_sec > 60 || @@ -97,7 +97,7 @@ inline int SonyFormatAsctime(char* buffer, std::size_t size, const GuestTm& time return EINVAL; } std::snprintf(buffer, size, "%.3s %.3s %2d %.2d:%.2d:%.2d %4d\n", days[time.tm_wday], months[time.tm_mon], - time.tm_mday, time.tm_hour, time.tm_min, time.tm_sec, year); + time.tm_mday, time.tm_hour, time.tm_min, time.tm_sec, static_cast(year)); return 0; } diff --git a/core/libs/prx/libc/include/WindowsDescriptor.hpp b/core/libs/prx/libc/include/WindowsDescriptor.hpp index 66ebc1d9ac..772a28f086 100644 --- a/core/libs/prx/libc/include/WindowsDescriptor.hpp +++ b/core/libs/prx/libc/include/WindowsDescriptor.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSDESCRIPTOR_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSDESCRIPTOR_HPP +#include "GuestFileNumber.hpp" #include #include #include @@ -29,8 +30,7 @@ static inline ACCESS_MASK WindowsHandleAccess_nid_no_patch(HANDLE handle, int de } static inline std::uint32_t WindowsFileNumber_nid_no_patch(std::uint64_t fileId) { - const std::uint32_t folded = static_cast(fileId) ^ (static_cast(fileId >> 32) * 0x9E3779B1u); - return folded == 0 ? 1 : folded; + return GuestFileNumber_nid_no_patch(fileId); } static inline std::uint32_t WindowsFileNumber_nid_no_patch(const BY_HANDLE_FILE_INFORMATION& identity) { diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index f0ee4f771d..ad07f41a7b 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -2,7 +2,9 @@ #define CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSFORMATTING_HPP #include "SceTypes.hpp" +#include #include +#include #include #include #include @@ -160,6 +162,288 @@ inline void AppendUtf8(std::string& utf8, char32_t code) { } } +struct FloatSpec { + bool left = false; + bool plus = false; + bool space = false; + bool alternate = false; + bool zero = false; + std::size_t width = 0; + int precision = -1; +}; + +inline FloatSpec ParseFloatSpec(const std::string& spec) { + FloatSpec result; + std::size_t index = 1; + for (; index < spec.size() && std::strchr("-+ #0", spec[index]); ++index) { + result.left |= spec[index] == '-'; + result.plus |= spec[index] == '+'; + result.space |= spec[index] == ' '; + result.alternate |= spec[index] == '#'; + result.zero |= spec[index] == '0'; + } + for (; index < spec.size() && spec[index] >= '0' && spec[index] <= '9'; ++index) { + const auto digit = static_cast(spec[index] - '0'); + if (result.width > (static_cast(INT_MAX) - digit) / 10) + throw std::overflow_error("Formatted output exceeds INT_MAX"); + result.width = result.width * 10 + digit; + } + if (index < spec.size() && spec[index] == '.') { + result.precision = 0; + for (++index; index < spec.size(); ++index) { + const int digit = spec[index] - '0'; + if (result.precision > (INT_MAX - digit) / 10) + throw std::overflow_error("Floating precision exceeds int range"); + result.precision = result.precision * 10 + digit; + } + } + return result; +} + +inline std::string PadFloat(const FloatSpec& spec, const std::string& prefix, const std::string& body) { + const std::size_t length = prefix.size() + body.size(); + if (length >= spec.width) return prefix + body; + const std::string padding(spec.width - length, spec.left || !spec.zero ? ' ' : '0'); + if (spec.left) return prefix + body + padding; + return spec.zero ? prefix + padding + body : padding + prefix + body; +} + +inline std::string HexDigits(const std::string& spec, char conversion, bool negative, std::uint64_t lead, std::uint64_t fraction, int total, int exponent, + std::size_t maximumLength) { + const FloatSpec parsed = ParseFloatSpec(spec); + const int precision = parsed.precision; + const bool upper = conversion == 'A'; + int digits = total; + if (precision >= 0 && precision < total) { + const int dropped = (total - precision) * 4; + const std::uint64_t remainder = dropped == 64 ? fraction : fraction & ((std::uint64_t{1} << dropped) - 1); + fraction = dropped == 64 ? 0 : fraction >> dropped; + if (remainder >= std::uint64_t{1} << (dropped - 1) && ++fraction >> (precision * 4)) { + fraction &= (std::uint64_t{1} << (precision * 4)) - 1; + ++lead; + } + digits = precision; + } else if (precision < 0) { + for (; digits > 0 && (fraction & 0xf) == 0; --digits) fraction >>= 4; + } + const char* hex = upper ? "0123456789ABCDEF" : "0123456789abcdef"; + std::string suffix = upper ? "P" : "p"; + suffix += exponent < 0 ? '-' : '+'; + suffix += std::to_string(exponent < 0 ? -exponent : exponent); + std::string prefix = negative ? "-" : parsed.plus ? "+" : parsed.space ? " " : ""; + prefix += upper ? "0X" : "0x"; + const auto fractionalDigits = static_cast(precision > total ? precision : digits); + const bool point = fractionalDigits != 0 || parsed.alternate; + const std::size_t length = prefix.size() + 1 + point + fractionalDigits + suffix.size(); + if (length > maximumLength || parsed.width > maximumLength) + throw std::overflow_error("Formatted output exceeds INT_MAX"); + std::string body(1, hex[lead]); + if (point) body += '.'; + for (int digit = digits - 1; digit >= 0; --digit) body += hex[(fraction >> (digit * 4)) & 0xf]; + if (precision > total) body.append(static_cast(precision - total), '0'); + body += suffix; + return PadFloat(parsed, prefix, body); +} + +inline std::string HexFloat(const std::string& spec, char conversion, double value, std::size_t maximumLength = INT_MAX) { + const std::uint64_t bits = std::bit_cast(value); + int exponent = static_cast((bits >> 52) & 0x7ff); + std::uint64_t fraction = bits & ((std::uint64_t{1} << 52) - 1); + std::uint64_t lead = 1; + if (exponent != 0) { + exponent -= 1023; + } else if (fraction == 0) { + lead = 0; + } else { + exponent = -1022; + while ((fraction & (std::uint64_t{1} << 52)) == 0) { + fraction <<= 1; + --exponent; + } + fraction &= (std::uint64_t{1} << 52) - 1; + } + return HexDigits(spec, conversion, (bits >> 63) != 0, lead, fraction, 13, exponent, maximumLength); +} + +inline std::string HexFloat(const std::string& spec, char conversion, long double value, std::size_t maximumLength = INT_MAX) { + if (value == 0) return HexDigits(spec, conversion, std::signbit(value), 0, 0, 16, 0, maximumLength); + int exponent = 0; + const auto mantissa = static_cast(std::ldexp(std::frexp(std::fabs(value), &exponent), 64)); + return HexDigits(spec, conversion, std::signbit(value), 1, mantissa << 1, 16, exponent - 1, maximumLength); +} + +struct DecimalDigits { + std::string digits; + long long exponent = 0; + bool truncated = false; + bool tiesDown = false; +}; + +inline std::string DecimalChunks(std::vector words) { + std::string text; + while (!words.empty()) { + std::uint64_t remainder = 0; + for (std::size_t index = words.size(); index-- > 0;) { + const std::uint64_t current = (remainder << 32) | words[index]; + words[index] = static_cast(current / 1000000000u); + remainder = current % 1000000000u; + } + while (!words.empty() && words.back() == 0) words.pop_back(); + std::string chunk = std::to_string(remainder); + if (!words.empty()) chunk.insert(0, 9 - chunk.size(), '0'); + text.insert(0, chunk); + } + return text; +} + +inline DecimalDigits ConsoleDecimalDigits(long double value) { + DecimalDigits result; + if (value == 0) return result; + int binary = 0; + const auto mantissa = static_cast(std::ldexp(std::frexp(std::fabs(value), &binary), 64)); + const long long estimate = static_cast(binary) * 30103 / 100000; + const long long last = (estimate >= 0 ? estimate / 4 : -((3 - estimate) / 4)) * 4 - 20; + result.tiesDown = estimate >= 8; + binary -= 64; + std::string integer; + std::string fraction; + bool remaining = false; + if (binary >= 0) { + const auto word = static_cast(binary) / 32; + const auto bit = static_cast(binary) % 32; + std::vector words(word + 3, 0); + for (std::size_t part = 0; part < 2; ++part) { + const std::uint64_t shifted = ((mantissa >> (32 * part)) & 0xffffffffu) << bit; + words[word + part] |= static_cast(shifted); + words[word + part + 1] |= static_cast(shifted >> 32); + } + while (!words.empty() && words.back() == 0) words.pop_back(); + integer = DecimalChunks(std::move(words)); + } else { + const auto shift = static_cast(-static_cast(binary)); + const std::uint64_t whole = shift >= 64 ? 0 : mantissa >> shift; + if (whole != 0) integer = std::to_string(whole); + std::vector words(shift / 32 + 2, 0); + const std::uint64_t remainder = shift >= 64 ? mantissa : mantissa & ((std::uint64_t{1} << shift) - 1); + words[0] = static_cast(remainder); + words[1] = static_cast(remainder >> 32); + const std::size_t top = shift / 32; + const auto bit = static_cast(shift % 32); + const auto significant = [&] { + const std::size_t leading = fraction.find_first_not_of('0'); + return integer.size() + (leading == std::string::npos ? 0 : fraction.size() - leading); + }; + remaining = remainder != 0; + while (remaining && significant() < 30) { + std::uint64_t carry = 0; + for (auto& word : words) { + const std::uint64_t product = static_cast(word) * 1000000000u + carry; + word = static_cast(product); + carry = product >> 32; + } + const std::uint64_t window = (static_cast(words[top + 1]) << 32) | words[top]; + const std::string chunk = std::to_string(static_cast(window >> bit)); + words[top + 1] = 0; + words[top] &= bit == 0 ? 0 : (std::uint32_t{1} << bit) - 1; + fraction += std::string(9 - chunk.size(), '0') + chunk; + remaining = std::any_of(words.begin(), words.end(), [](std::uint32_t word) { return word != 0; }); + } + } + if (!integer.empty()) { + result.exponent = static_cast(integer.size()) - 1; + result.digits = integer + fraction; + } else { + const std::size_t leading = fraction.find_first_not_of('0'); + result.exponent = -static_cast(leading) - 1; + result.digits = fraction.substr(leading); + } + const long long kept = result.exponent - last + 1; + if (static_cast(result.digits.size()) > kept) { + result.truncated = result.digits.find_first_not_of('0', static_cast(kept)) != std::string::npos; + result.digits.resize(static_cast(kept)); + } + result.truncated |= remaining; + return result; +} + +inline DecimalDigits RoundDecimal(const DecimalDigits& value, long long count) { + if (value.digits.empty()) return value; + if (count <= 0) { + if (count == 0 && value.digits[0] >= '5') return {"1", value.exponent + 1}; + return {}; + } + if (count >= static_cast(value.digits.size())) return value; + DecimalDigits result{value.digits.substr(0, static_cast(count)), value.exponent}; + const auto next = static_cast(count); + if (value.digits[next] < '5') return result; + if (value.digits[next] == '5' && value.tiesDown && !value.truncated && value.digits.find_first_not_of('0', next + 1) == std::string::npos) + return result; + for (std::size_t index = result.digits.size(); index-- > 0;) { + if (result.digits[index] != '9') { + ++result.digits[index]; + return result; + } + result.digits[index] = '0'; + } + result.digits.insert(result.digits.begin(), '1'); + result.digits.pop_back(); + ++result.exponent; + return result; +} + +inline char DigitAt(const DecimalDigits& value, long long position) { + const long long index = value.exponent - position; + if (value.digits.empty() || index < 0 || index >= static_cast(value.digits.size())) return '0'; + return value.digits[static_cast(index)]; +} + +inline std::string DecimalFloat(const std::string& spec, char conversion, long double value, std::size_t maximumLength = INT_MAX) { + const FloatSpec parsed = ParseFloatSpec(spec); + const DecimalDigits exact = ConsoleDecimalDigits(value); + const bool upper = conversion == 'E' || conversion == 'G'; + const char lower = static_cast(conversion | 0x20); + long long precision = parsed.precision < 0 ? 6 : parsed.precision; + bool exponential = lower == 'e'; + bool strip = false; + if (lower == 'g') { + if (precision == 0) precision = 1; + const long long exponent = exact.digits.empty() ? 0 : RoundDecimal(exact, precision).exponent; + exponential = !(precision > exponent && exponent >= -4); + precision = exponential ? precision - 1 : precision - 1 - exponent; + strip = !parsed.alternate; + } + const DecimalDigits rounded = RoundDecimal(exact, exponential ? precision + 1 : exact.exponent + 1 + precision); + const long long exponent = rounded.digits.empty() ? 0 : rounded.exponent; + if (strip) { + const long long last = rounded.digits.empty() ? 0 : rounded.exponent - static_cast(rounded.digits.size()) + 1; + long long needed = exponential ? exponent - last : -last; + while (needed > 0 && DigitAt(rounded, (exponential ? exponent : 0) - needed) == '0') --needed; + precision = std::max(0LL, std::min(precision, needed)); + } + const std::string prefix = std::signbit(value) ? "-" : parsed.plus ? "+" : parsed.space ? " " : ""; + const bool point = precision > 0 || parsed.alternate; + const std::string magnitude = std::to_string(exponent < 0 ? -exponent : exponent); + const long long whole = exponential || exponent < 0 ? 1 : exponent + 1; + const long long exponentLength = exponential ? 2 + static_cast(std::max(magnitude.size(), 2)) : 0; + const long long length = static_cast(prefix.size()) + whole + point + precision + exponentLength; + if (length > static_cast(maximumLength) || parsed.width > maximumLength) + throw std::overflow_error("Formatted output exceeds INT_MAX"); + std::string body; + body.reserve(static_cast(length)); + const long long first = exponential ? exponent : std::max(exponent, 0LL); + for (long long position = first; position >= (exponential ? exponent : 0); --position) body += DigitAt(rounded, position); + if (point) body += '.'; + const long long start = exponential ? exponent - 1 : -1; + for (long long position = start; position > start - precision; --position) body += DigitAt(rounded, position); + if (exponential) { + body += upper ? 'E' : 'e'; + body += exponent < 0 ? '-' : '+'; + if (magnitude.size() < 2) body += '0'; + body += magnitude; + } + return PadFloat(parsed, prefix, body); +} + inline std::string PaddedStringSpec(const std::string& spec) { std::string result = "%"; std::size_t index = 1; @@ -373,13 +657,29 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo static_assert(sizeof(long double) == 16); static_assert(std::numeric_limits::digits == 64); const long double value = args.Next(); - if (std::isnan(value)) output.Value(PaddedStringSpec(spec), NanText(spec, std::signbit(value), conversion < 'a').c_str()); - else output.Value(spec + "L" + conversion, value); + if (std::isnan(value)) { + output.Value(PaddedStringSpec(spec), NanText(spec, std::signbit(value), conversion < 'a').c_str()); + } else if (std::isfinite(value)) { + const auto remaining = static_cast(INT_MAX - output.Count()); + const std::string text = conversion == 'a' || conversion == 'A' ? HexFloat(spec, conversion, value, remaining) : DecimalFloat(spec, conversion, value, remaining); + output.Append(text.data(), text.size()); + } else { + output.Value(spec + "L" + conversion, value); + } } else { if (!length.empty() && length != "l") throw std::invalid_argument("Invalid floating length"); const double value = args.Next(); - if (std::isnan(value)) output.Value(PaddedStringSpec(spec), NanText(spec, std::signbit(value), conversion < 'a').c_str()); - else output.Value(spec + conversion, value); + if (std::isnan(value)) { + output.Value(PaddedStringSpec(spec), NanText(spec, std::signbit(value), conversion < 'a').c_str()); + } else if ((conversion == 'a' || conversion == 'A') && std::isfinite(value)) { + const std::string text = HexFloat(spec, conversion, value, static_cast(INT_MAX - output.Count())); + output.Append(text.data(), text.size()); + } else if (std::isfinite(value)) { + const std::string text = DecimalFloat(spec, conversion, value, static_cast(INT_MAX - output.Count())); + output.Append(text.data(), text.size()); + } else { + output.Value(spec + conversion, value); + } } } else if (conversion == 'c' && (length.empty() || length == "h")) { output.Value(spec + conversion, args.Next()); diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 9e348301a8..7a7a3f7869 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -136,6 +136,8 @@ public: const auto shared = slot.page.lock(); return resident && !slot.stored && (!shared || shared->aliases.empty()); }; + const bool perPage = !resident && sectionBytes == SIZE_MAX; + if (perPage) commitSection(section, offset, bytes); for (std::size_t done = 0; done < bytes;) { const auto base = cursor + done; if (single(done)) { @@ -165,23 +167,56 @@ public: done += runBytes; continue; } - const auto slot = slotOf(done); - const auto shared = pageOf(*slot); - const auto aliases = shared->aliases; - for (const auto alias : aliases) { - if (views.at(alias).resident) demote(alias); - views.at(alias).aliasDemoted = false; + if (sectionBytes != SIZE_MAX) { + const auto slot = slotOf(done); + const auto shared = pageOf(*slot); + const auto aliases = shared->aliases; + for (const auto alias : aliases) { + if (views.at(alias).resident) demote(alias); + views.at(alias).aliasDemoted = false; + } + const auto viewBytes = std::min(pageBytes, sectionBytes - done); + split(base, pageBytes); + mapShared(base, section, offset + done, protection, viewProtection, viewBytes); + slot->stored = true; + shared->aliases.push_back(base); + const auto created = views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0, 0, false, slot, hostPages(viewBytes)}).first; + if (shared->comparedWhenShared) created->second.unreported = allSubpages; + remember(freshRanges, base, base + pageBytes); + invalidate(*shared); + done += pageBytes; + continue; } - const auto viewBytes = std::min(pageBytes, sectionBytes - done); - split(base, pageBytes); - mapShared(base, section, offset + done, protection, viewProtection, viewBytes); - slot->stored = true; - shared->aliases.push_back(base); - const auto created = views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0, 0, false, slot, hostPages(viewBytes)}).first; - if (shared->comparedWhenShared) created->second.unreported = allSubpages; - remember(freshRanges, base, base + pageBytes); - invalidate(*shared); - done += pageBytes; + std::vector, std::shared_ptr>> pages; + pages.reserve((bytes - done) / pageBytes); + auto runBytes = std::size_t{0}; + do { + const auto slot = slotOf(done + runBytes); + const auto shared = pageOf(*slot); + const auto aliases = shared->aliases; + for (const auto alias : aliases) { + if (views.at(alias).resident) demote(alias); + views.at(alias).aliasDemoted = false; + } + pages.emplace_back(slot, shared); + runBytes += pageBytes; + } while (done + runBytes < bytes && !single(done + runBytes)); + if (!perPage) commitSection(section, offset + done, runBytes); + split(base, runBytes); + mapRun(base, runBytes, section, offset + done); + DWORD previous; + if (!VirtualProtect(reinterpret_cast(base), runBytes, protection, &previous)) fail("protect shared guest pages"); + collectableRanges.clear(); + for (std::size_t at = 0; at < runBytes; at += pageBytes) { + auto& [slot, shared] = pages[at / pageBytes]; + slot->stored = true; + shared->aliases.push_back(base + at); + invalidate(*shared); + const auto created = views.emplace(base + at, View{std::move(shared), protection, {}, 0, owned, offset + done + at, 0, 0, false, std::move(slot)}).first; + if (created->second.page->comparedWhenShared) created->second.unreported = allSubpages; + } + remember(freshRanges, base, base + runBytes); + done += runBytes; } } @@ -264,15 +299,11 @@ public: const auto found = views.find(base); if (found == views.end() || !writable(found->second.protection)) return false; auto& view = found->second; - const auto written = subpagesOf(base, address, address + 1); - if (view.resident && (view.armed & written) == 0) { + if (view.resident) { const auto memory = query(address); return memory.State == MEM_COMMIT && writable(memory.Protect & 0xffu); } - if (view.resident) { - resumeResidentWrites(base); - return true; - } + const auto written = subpagesOf(base, address, address + 1); invalidate(*view.page, written); protectSubpages(base, written, view.protection, "resume shared memory write", view.viewBytes); view.armed &= static_cast(~written); @@ -651,7 +682,6 @@ private: std::uintptr_t end; std::array collected; std::uint32_t fresh; - bool writeObserved = false; }; struct Window { void* view = nullptr; @@ -665,14 +695,12 @@ private: void collectResident(std::uintptr_t start, Run& run, std::uintptr_t from, std::uintptr_t stop, void** pages, std::size_t* count, bool clear, Collection collection) { const auto first = from & ~std::uintptr_t{4095}; - const bool armed = clear && run.fresh == 0 && !run.writeObserved && collection != Collection::Fresh && run.end - start >= 0x40000 && armResident(first, stop); if (run.fresh == 0 && collection != Collection::Fresh) { ULONG_PTR reported = (stop - first + 4095) / 4096; DWORD granularity = 0; if (GetWriteWatch(clear ? WRITE_WATCH_FLAG_RESET : 0, reinterpret_cast(first), stop - first, pages + *count, &reported, &granularity) != 0) fail("collect resident guest writes"); for (ULONG_PTR index = 0; index < reported; ++index) pages[*count + index] = reinterpret_cast(std::max(from, reinterpret_cast(pages[*count + index]))); *count += reported; - if (armed) rememberClean(from, stop); return; } std::vector written; @@ -710,44 +738,6 @@ private: if (collection == Collection::Fresh) flushReset(stop); } - void resumeResidentWrites(std::uintptr_t address) { - const auto run = std::prev(residentRuns.upper_bound(address)); - run->second.writeObserved = true; - forgetClean(run->first, run->second.end); - auto first = views.lower_bound(run->first); - while (first != views.end() && first->first < run->second.end) { - const auto begin = first->first; - auto end = begin; - const auto protection = first->second.protection; - auto next = first; - bool armed = false; - do { - armed = armed || next->second.armed != 0; - end = next->first + pageBytes; - ++next; - } while (next != views.end() && next->first == end && end < run->second.end && next->second.protection == protection); - if (armed) { - DWORD previous; - if (!VirtualProtect(reinterpret_cast(begin), end - begin, protection, &previous)) fail("resume resident guest writes"); - } - for (; first != next; ++first) first->second.armed = 0; - } - } - - bool armResident(std::uintptr_t from, std::uintptr_t stop) { - const auto first = views.lower_bound(from & ~(pageBytes - 1)); - if (first == views.end() || !writable(first->second.protection)) return false; - const auto protection = first->second.protection; - for (auto it = first; it != views.end() && it->first < stop; ++it) { - if (!it->second.resident || it->second.protection != protection || it->second.hostWrites != 0 || it->second.hostImports != 0 || it->second.page->pins != 0) return false; - } - DWORD previous; - const auto readOnly = protection == PAGE_EXECUTE_READWRITE ? PAGE_EXECUTE_READ : PAGE_READONLY; - if (!VirtualProtect(reinterpret_cast(from), stop - from, readOnly, &previous)) fail("arm resident memory write tracking"); - for (auto it = first; it != views.end() && it->first < stop; ++it) it->second.armed |= subpagesOf(it->first, from, stop); - return true; - } - Window open(HANDLE section, std::uint64_t offset, std::size_t bytes) { const auto lead = offset % granularity; Window window; @@ -771,12 +761,23 @@ private: return (bytes + subpageBytes - 1) & ~(subpageBytes - 1); } - void mapShared(std::uintptr_t base, HANDLE section, std::uint64_t offset, DWORD protection, DWORD viewProtection = PAGE_EXECUTE_READWRITE, std::size_t sectionBytes = pageBytes) { + void commitSection(HANDLE section, std::uint64_t offset, std::size_t bytes) { + auto window = open(section, offset, bytes); + const auto memory = query(reinterpret_cast(window.bytes)); + if ((memory.State != MEM_COMMIT || memory.RegionSize < bytes) && !VirtualAlloc(window.bytes, bytes, MEM_COMMIT, PAGE_READWRITE)) { + const auto error = GetLastError(); + close(window); + throw std::system_error(static_cast(error), std::system_category(), "commit direct memory pages"); + } + close(window); + } + + void mapShared(std::uintptr_t base, HANDLE section, std::uint64_t offset, DWORD protection, DWORD viewProtection = PAGE_EXECUTE_READWRITE, std::size_t sectionBytes = pageBytes, bool committed = false) { void* page = reinterpret_cast(base); const auto viewBytes = hostPages(sectionBytes); if (viewBytes != pageBytes) split(base, viewBytes); if (!map(section, GetCurrentProcess(), page, offset, sectionBytes, MEM_REPLACE_PLACEHOLDER, viewProtection, nullptr, 0)) fail("map shared guest page"); - if (query(base).State == MEM_RESERVE && !VirtualAlloc(page, viewBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page"); + if (!committed && query(base).State == MEM_RESERVE && !VirtualAlloc(page, viewBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page"); DWORD previous; if (!VirtualProtect(page, viewBytes, protection, &previous)) fail("protect shared guest page"); if (viewBytes == pageBytes) return; @@ -785,6 +786,47 @@ private: if (!VirtualProtect(zeros, pageBytes - viewBytes, protection, &previous)) fail("protect guest pages past the end of a file"); } + void mapRun(std::uintptr_t base, std::size_t bytes, HANDLE section, std::uint64_t offset) { + if (!map(section, GetCurrentProcess(), reinterpret_cast(base), offset, bytes, MEM_REPLACE_PLACEHOLDER, PAGE_EXECUTE_READWRITE, nullptr, 0)) fail("map a run of shared guest pages"); + } + + void breakView(std::uintptr_t at) { + const auto memory = query(at); + if (memory.State == MEM_FREE || memory.Type != MEM_MAPPED) return; + const auto start = reinterpret_cast(memory.AllocationBase); + if (start == at) return; + const auto first = views.find(start); + if (first == views.end()) return; + struct Part { + std::uintptr_t base; + std::uintptr_t end; + DWORD protection; + bool committed; + }; + std::vector parts; + auto stop = start; + for (;;) { + const auto part = query(stop); + if (part.AllocationBase != memory.AllocationBase) break; + const auto partEnd = reinterpret_cast(part.BaseAddress) + part.RegionSize; + parts.push_back({stop, partEnd, part.Protect, part.State == MEM_COMMIT}); + stop = partEnd; + } + const auto section = first->second.section; + const auto offset = first->second.offset; + if (!unmap(GetCurrentProcess(), reinterpret_cast(start), MEM_PRESERVE_PLACEHOLDER)) fail("unmap a run of shared guest pages to split it"); + split(start, at - start); + mapRun(start, at - start, section->handle, offset); + mapRun(at, stop - at, section->handle, offset + (at - start)); + for (const auto& part : parts) { + if (!part.committed) continue; + for (const auto [from, to] : {std::pair{part.base, std::min(part.end, at)}, std::pair{std::max(part.base, at), part.end}}) { + DWORD previous; + if (from < to && !VirtualProtect(reinterpret_cast(from), to - from, part.protection, &previous)) fail("protect a split run of shared guest pages"); + } + } + } + static bool blank(std::uintptr_t page) { const auto* words = reinterpret_cast(page); for (std::size_t i = 0; i < pageBytes / sizeof(std::uint64_t); ++i) { @@ -976,7 +1018,7 @@ private: return memory; } - static void split(std::uintptr_t address, std::size_t bytes) { + static std::uintptr_t split(std::uintptr_t address, std::size_t bytes) { auto memory = query(address); memory = query(reinterpret_cast(memory.AllocationBase)); if (memory.State != MEM_RESERVE) throw std::runtime_error("guest mapping requires a placeholder"); @@ -987,6 +1029,7 @@ private: } if (memory.RegionSize < bytes) throw std::runtime_error("guest placeholder is too small"); if (memory.RegionSize != bytes && !VirtualFree(reinterpret_cast(address), bytes, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("split guest placeholder suffix"); + return address + memory.RegionSize; } void reset(std::uintptr_t address, std::size_t bytes) { @@ -995,6 +1038,8 @@ private: forgetClean(address, end); breakRun(address); breakRun(end); + breakView(address); + breakView(end); if (const auto run = residentRuns.find(address); run != residentRuns.end() && run->second.end == end) { const auto memory = query(address); if (reinterpret_cast(memory.AllocationBase) != address) throw std::runtime_error("cannot release part of a host allocation"); @@ -1026,12 +1071,11 @@ private: if (allocationEnd > end || query(allocationEnd).AllocationBase == memory.AllocationBase) throw std::runtime_error("guest release truncates a host allocation"); if (memory.Type == MEM_MAPPED) { if (!unmap(GetCurrentProcess(), reinterpret_cast(cursor), MEM_PRESERVE_PLACEHOLDER)) fail("unmap shared guest page"); - const auto found = views.find(cursor); - if (found != views.end()) { - std::erase(found->second.page->aliases, cursor); - views.erase(found); - forget(freshRanges, cursor, cursor + pageBytes); + for (auto found = views.lower_bound(cursor); found != views.end() && found->first < allocationEnd;) { + std::erase(found->second.page->aliases, found->first); + found = views.erase(found); } + forget(freshRanges, cursor, allocationEnd); } else if (memory.Type == MEM_PRIVATE) { if (const auto run = residentRuns.find(cursor); run != residentRuns.end()) evict(run); if (!VirtualFree(reinterpret_cast(cursor), allocationEnd - cursor, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release private guest memory"); diff --git a/core/libs/prx/libc/include/WindowsScanning.hpp b/core/libs/prx/libc/include/WindowsScanning.hpp index 77bbf1541e..ab3f6ef625 100644 --- a/core/libs/prx/libc/include/WindowsScanning.hpp +++ b/core/libs/prx/libc/include/WindowsScanning.hpp @@ -16,15 +16,30 @@ inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* sou std::vector pointers; std::vector suppressedEnds(static_cast(std::count(format, format + std::strlen(format), '%')), -1); size_t suppressedCount = 0; + struct NarrowInteger { + void* destination; + size_t size; + bool isUnsigned; + long long value; + unsigned long long unsignedValue; + int end; + }; + std::vector narrowIntegers; + narrowIntegers.reserve(suppressedEnds.size()); FormatArguments args(source); while (*format) { const char value = *format++; translated += value; if (value != '%') continue; if (*format == '%') { translated += *format++; continue; } + const size_t directive = translated.size() - 1; const bool suppressed = *format == '*'; if (suppressed) translated += *format++; while (*format >= '0' && *format <= '9') translated += *format++; + if (*format == 'q') { + translated.resize(directive); + break; + } std::string length; if (*format && std::strchr("hljztL", *format)) { length += *format++; @@ -37,7 +52,8 @@ inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* sou return EOF; } ++format; - if (std::strchr("diouxXn", conversion) && (length == "l" || length == "j" || length == "z" || length == "t")) + 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; @@ -48,13 +64,30 @@ inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* sou if (*format != ']') { errno = 22; return EOF; } translated += *format++; } - if (!suppressed) pointers.push_back(args.Next()); + 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}); + translated += "%n"; + auto& integer = narrowIntegers.back(); + pointers.push_back(isUnsigned ? static_cast(&integer.unsignedValue) : static_cast(&integer.value)); + pointers.push_back(&integer.end); + } else if (!suppressed) pointers.push_back(args.Next()); else if (conversion != 'n') { translated += "%n"; pointers.push_back(&suppressedEnds[suppressedCount++]); } } const int result = scan(translated.c_str(), reinterpret_cast(pointers.data())); + for (const auto& integer : narrowIntegers) { + if (integer.end < 0) continue; + if (integer.isUnsigned) { + if (integer.size == sizeof(char)) *static_cast(integer.destination) = static_cast(integer.unsignedValue); + else if (integer.size == sizeof(short)) *static_cast(integer.destination) = static_cast(integer.unsignedValue); + else *static_cast(integer.destination) = static_cast(integer.unsignedValue); + } else if (integer.size == sizeof(char)) *static_cast(integer.destination) = static_cast(integer.value); + else if (integer.size == sizeof(short)) *static_cast(integer.destination) = static_cast(integer.value); + else *static_cast(integer.destination) = static_cast(integer.value); + } if (result == EOF && std::any_of(suppressedEnds.begin(), suppressedEnds.begin() + suppressedCount, [](int end) { return end >= 0; })) return 0; return result; diff --git a/core/libs/prx/libc/src/AppArchive.cpp b/core/libs/prx/libc/src/AppArchive.cpp index ba32feae07..8195fed12f 100644 --- a/core/libs/prx/libc/src/AppArchive.cpp +++ b/core/libs/prx/libc/src/AppArchive.cpp @@ -129,17 +129,50 @@ std::vector AppArchiveList_nid_no_patch(std::uint32_t for (std::uint32_t index = 0; index < count; ++index) { ZArchiveReader::DirEntry entry{}; if (!reader.GetDirEntry(handle, index, entry)) throw std::runtime_error("app0.zar is damaged: directory entry " + std::to_string(index) + " cannot be read"); - entries.push_back({std::string(entry.name), entry.isDirectory}); + entries.push_back({std::string(entry.name), entry.isDirectory, 0, entry.size}); } return entries; } +std::vector AppArchiveListDirectory_nid_no_patch(const std::filesystem::path& hostPath) { + AppArchiveNode directory{}; + if (!AppArchiveLookup_nid_no_patch(hostPath, &directory) || !directory.Directory) + throw std::invalid_argument("AppArchiveListDirectory: path is not an app0.zar directory"); + auto entries = AppArchiveList_nid_no_patch(directory.Handle); + for (auto& entry : entries) { + AppArchiveNode node{}; + std::u8string name; + name.reserve(entry.Name.size()); + for (const unsigned char value : entry.Name) name.push_back(static_cast(value)); + if (!AppArchiveLookup_nid_no_patch(hostPath / std::filesystem::path(name), &node)) + throw std::runtime_error("app0.zar is damaged: directory child " + entry.Name + " cannot be looked up"); + entry.Handle = node.Handle; + entry.Size = node.Size; + } + return entries; +} + +std::vector AppArchiveListUnshadowedDirectory_nid_no_patch(const std::filesystem::path& hostPath, const std::vector& hostNames) { + auto entries = AppArchiveListDirectory_nid_no_patch(hostPath); + entries.erase(std::remove_if(entries.begin(), entries.end(), [&hostNames](const AppArchiveDirectoryEntry& entry) { + return std::any_of(hostNames.begin(), hostNames.end(), [&entry](const std::string& name) { + return SameComponent(name, entry.Name); + }); + }), entries.end()); + return entries; +} + +bool AppArchiveNameMatches_nid_no_patch(const std::string& left, const std::string& right) { + return SameComponent(left, right); +} + void AppArchiveReady_nid_no_patch() { (void)Opened(); } void AppArchiveRefuseOnly_nid_no_patch(const std::filesystem::path& hostPath, const char* guestPath, const char* operation) { - if (AppArchiveOnly_nid_no_patch(hostPath)) NotImplemented_nid_no_patch((std::string(operation) + " " + guestPath + " in app0.zar").c_str()); + if (!AppArchiveOnly_nid_no_patch(hostPath)) return; + NotImplemented_nid_no_patch((std::string(operation) + " " + guestPath + " in app0.zar").c_str()); } void AppArchiveRefuseMergedListing_nid_no_patch(const std::filesystem::path& hostPath, const char* guestPath) { diff --git a/core/libs/prx/libc/src/DinkumwareAtomics.cpp b/core/libs/prx/libc/src/DinkumwareAtomics.cpp index 13af94ddea..fc480d7c02 100644 --- a/core/libs/prx/libc/src/DinkumwareAtomics.cpp +++ b/core/libs/prx/libc/src/DinkumwareAtomics.cpp @@ -63,6 +63,18 @@ int LockedCompareExchange(volatile unsigned char* flag, std::size_t size, volati extern "C" { +std::uint8_t APS5_VABI _Fetch_and_seq_cst_1_nid_postfix(volatile std::uint8_t* target, std::uint8_t value) { return __atomic_fetch_and(target, value, __ATOMIC_SEQ_CST); } +std::uint16_t APS5_VABI _Fetch_and_seq_cst_2_nid_postfix(volatile std::uint16_t* target, std::uint16_t value) { return __atomic_fetch_and(target, value, __ATOMIC_SEQ_CST); } +std::uint32_t APS5_VABI _Fetch_and_seq_cst_4_nid_postfix(volatile std::uint32_t* target, std::uint32_t value) { return __atomic_fetch_and(target, value, __ATOMIC_SEQ_CST); } + +std::uint8_t APS5_VABI _Fetch_or_seq_cst_1_nid_postfix(volatile std::uint8_t* target, std::uint8_t value) { return __atomic_fetch_or(target, value, __ATOMIC_SEQ_CST); } +std::uint16_t APS5_VABI _Fetch_or_seq_cst_2_nid_postfix(volatile std::uint16_t* target, std::uint16_t value) { return __atomic_fetch_or(target, value, __ATOMIC_SEQ_CST); } +std::uint32_t APS5_VABI _Fetch_or_seq_cst_4_nid_postfix(volatile std::uint32_t* target, std::uint32_t value) { return __atomic_fetch_or(target, value, __ATOMIC_SEQ_CST); } + +std::uint8_t APS5_VABI _Fetch_xor_seq_cst_1_nid_postfix(volatile std::uint8_t* target, std::uint8_t value) { return __atomic_fetch_xor(target, value, __ATOMIC_SEQ_CST); } +std::uint16_t APS5_VABI _Fetch_xor_seq_cst_2_nid_postfix(volatile std::uint16_t* target, std::uint16_t value) { return __atomic_fetch_xor(target, value, __ATOMIC_SEQ_CST); } +std::uint32_t APS5_VABI _Fetch_xor_seq_cst_4_nid_postfix(volatile std::uint32_t* target, std::uint32_t value) { return __atomic_fetch_xor(target, value, __ATOMIC_SEQ_CST); } + void APS5_VABI _Atomic_store_1_nid_postfix(volatile std::uint8_t* target, std::uint8_t value, int) { Store(target, value); } void APS5_VABI _Atomic_store_2_nid_postfix(volatile std::uint16_t* target, std::uint16_t value, int) { Store(target, value); } void APS5_VABI _Atomic_store_4_nid_postfix(volatile std::uint32_t* target, std::uint32_t value, int) { Store(target, value); } diff --git a/core/libs/prx/libc/src/DinkumwareInternals.cpp b/core/libs/prx/libc/src/DinkumwareInternals.cpp index d1f8ac33f4..dc45984759 100644 --- a/core/libs/prx/libc/src/DinkumwareInternals.cpp +++ b/core/libs/prx/libc/src/DinkumwareInternals.cpp @@ -1,9 +1,13 @@ #include "prx/libc/include/Api.hpp" #include #include +#include +#include #include #include #include +#include +#include #include #include "prx/libc/include/General.hpp" @@ -73,6 +77,67 @@ TInteger ParsedInteger(const char* text, char** end, int base, int* error, TInte return value; } +constexpr short Denormal = -2, Finite = -1, Zero = 0, Infinite = 1, NotANumber = 2; + +void RaiseLibcError(int exceptions) { + if ((exceptions & (FE_OVERFLOW | FE_UNDERFLOW)) != 0) exceptions |= FE_INEXACT; + std::feraiseexcept(exceptions); + const bool range = (exceptions & FE_INVALID) == 0 && + (exceptions & (FE_DIVBYZERO | FE_OVERFLOW | FE_UNDERFLOW)) != 0; + errno = range ? ERANGE : EDOM; +} + +template +short ClassifyValue(TFloat value) { + switch (std::fpclassify(value)) { + case FP_NAN: + return NotANumber; + case FP_INFINITE: + return Infinite; + case FP_ZERO: + return Zero; + case FP_SUBNORMAL: + return Denormal; + default: + return Finite; + } +} + +template +short ScaleProduct(TFloat& value, TFloat factor, TFloat multiplier, long exponent) { + std::fexcept_t saved; + std::fegetexceptflag(&saved, FE_ALL_EXCEPT); + const int savedErrno = errno; + TFloat result = factor * multiplier; + if (!std::isfinite(result) || result == static_cast(0)) { + int factorExponent = 0; + int multiplierExponent = 0; + const TFloat factorMantissa = std::frexp(factor, &factorExponent); + const TFloat multiplierMantissa = std::frexp(multiplier, &multiplierExponent); + const long long combinedExponent = static_cast(factorExponent) + multiplierExponent + exponent; + result = std::scalbn(factorMantissa * multiplierMantissa, static_cast(combinedExponent)); + } else { + result = std::scalbn(result, static_cast(exponent)); + } + value = result; + const short kind = ClassifyValue(result); + const int raised = std::fetestexcept(FE_ALL_EXCEPT) & ~static_cast(saved) & FE_ALL_EXCEPT; + std::fesetexceptflag(&saved, FE_ALL_EXCEPT); + errno = savedErrno; + if (kind == Infinite) { + RaiseLibcError(FE_OVERFLOW); + return Infinite; + } + if (kind == Zero) { + RaiseLibcError(FE_UNDERFLOW); + return Zero; + } + if (kind == Denormal) return Finite; + if (kind == NotANumber) return NotANumber; + if ((raised & FE_INEXACT) != 0) std::feraiseexcept(FE_INEXACT); + return Finite; +} + } extern "C" { @@ -131,6 +196,200 @@ long long APS5_VABI _Stoll_nid_postfix(const char* text, char** end, int base) { return _Stollx_nid_postfix(text, end, base, nullptr); } +short APS5_VABI _Exp_nid_postfix(double* value, double y, short offset) { + if (value == nullptr) throw std::invalid_argument("_Exp: null value"); + std::fexcept_t entryFlags; + std::fegetexceptflag(&entryFlags, FE_ALL_EXCEPT); + const int entryErrno = errno; + const double x = *value; + constexpr double HugeExp = 1842.0; + constexpr double Eps = std::numeric_limits::epsilon(); + constexpr double InverseLn2 = 1.4426950408889634073599246810018921; + constexpr double Ln2High = 22713.0 / 32768.0; + constexpr double Ln2Low = 1.4286068203094172321214581765680755e-6; + constexpr double P1 = 420.30235984910635; + constexpr double P2 = 15132.70094680474802; + constexpr double Q0 = 30.01511290683317; + constexpr double Q1 = 3362.72154416553028; + constexpr double Q2 = 30265.40189360949691; + constexpr double E = 0x1.5bf0a8b145769p+1; + const short xKind = ClassifyValue(x); + const short yKind = ClassifyValue(y); + if (xKind == NotANumber) return NotANumber; + if (yKind == NotANumber) return NotANumber; + if (xKind == Infinite) { + if (x > 0.0) { + if (yKind == Zero) { + *value = std::numeric_limits::quiet_NaN(); + RaiseLibcError(FE_INVALID); + return NotANumber; + } + if (yKind == Infinite) { + *value = y; + } else { + *value = std::copysign(std::numeric_limits::infinity(), y); + } + return Infinite; + } + if (yKind == Infinite) { + *value = std::numeric_limits::quiet_NaN(); + RaiseLibcError(FE_INVALID); + return NotANumber; + } + *value = std::copysign(0.0, y); + return Zero; + } + if (yKind == Zero) { + *value = y; + return Zero; + } + if (yKind == Infinite) { + *value = y; + return Infinite; + } + if (xKind == Zero) { + if (offset == 0) { + *value = y; + return Finite; + } + const short result = ScaleProduct(*value, 1.0, y, offset); + if (result == Finite && std::fpclassify(*value) == FP_SUBNORMAL) { + std::fesetexceptflag(&entryFlags, FE_ALL_EXCEPT); + errno = entryErrno; + } + return result; + } + if (offset == 0 && x == 1.0 && y == 1.0) { + *value = E; + return Finite; + } + if (x < -HugeExp) { + *value = std::copysign(0.0, y); + return Zero; + } + if (x > HugeExp) { + *value = std::copysign(std::numeric_limits::infinity(), y); + RaiseLibcError(FE_OVERFLOW); + return Infinite; + } + double g = x * InverseLn2; + long exponent = static_cast(g + (g < 0.0 ? -0.5 : 0.5)); + g = static_cast(exponent); + g = (x - g * Ln2High) - g * Ln2Low; + double factor; + if (-Eps < g && g < Eps) { + factor = 1.0; + } else { + const double z = g * g; + const double w = (Q0 * z + Q1) * z + Q2; + g *= (z + P1) * z + P2; + factor = (w + g) / (w - g) * 2.0; + --exponent; + } + const short result = ScaleProduct(*value, factor, y, exponent + offset); + if (result == Finite && std::fpclassify(*value) == FP_SUBNORMAL) { + std::fesetexceptflag(&entryFlags, FE_ALL_EXCEPT); + errno = entryErrno; + } + return result; +} + +short APS5_VABI _FExp_nid_postfix(float* value, float y, short offset) { + if (value == nullptr) throw std::invalid_argument("_FExp: null value"); + std::fexcept_t entryFlags; + std::fegetexceptflag(&entryFlags, FE_ALL_EXCEPT); + const int entryErrno = errno; + const float x = *value; + constexpr float HugeExp = 229.0f; + constexpr float Eps = std::numeric_limits::epsilon(); + constexpr float InverseLn2 = 1.4426950216f; + constexpr float Ln2High = 0.69287109375f; + constexpr float Ln2Low = 0.00027608682f; + constexpr float Q = 0.09997485578f; + constexpr float P0 = 0.5f; + constexpr float P1 = 0.0083208261f; + constexpr float E = 0x1.5bf0a8p+1f; + const short xKind = ClassifyValue(x); + const short yKind = ClassifyValue(y); + if (xKind == NotANumber) return NotANumber; + if (yKind == NotANumber) return NotANumber; + if (xKind == Infinite) { + if (x > 0.0f) { + if (yKind == Zero) { + *value = std::numeric_limits::quiet_NaN(); + RaiseLibcError(FE_INVALID); + return NotANumber; + } + if (yKind == Infinite) { + *value = y; + } else { + *value = std::copysign(std::numeric_limits::infinity(), y); + } + return Infinite; + } + if (yKind == Infinite) { + *value = std::numeric_limits::quiet_NaN(); + RaiseLibcError(FE_INVALID); + return NotANumber; + } + *value = std::copysign(0.0f, y); + return Zero; + } + if (yKind == Zero) { + *value = y; + return Zero; + } + if (yKind == Infinite) { + *value = y; + return Infinite; + } + if (xKind == Zero) { + if (offset == 0) { + *value = y; + return Finite; + } + const short result = ScaleProduct(*value, 1.0f, y, offset); + if (result == Finite && std::fpclassify(*value) == FP_SUBNORMAL) { + std::fesetexceptflag(&entryFlags, FE_ALL_EXCEPT); + errno = entryErrno; + } + return result; + } + if (offset == 0 && x == 1.0f && y == 1.0f) { + *value = E; + return Finite; + } + if (x < -HugeExp) { + *value = std::copysign(0.0f, y); + return Zero; + } + if (x > HugeExp) { + *value = std::copysign(std::numeric_limits::infinity(), y); + RaiseLibcError(FE_OVERFLOW); + return Infinite; + } + float g = x * InverseLn2; + long exponent = static_cast(g + (g < 0.0f ? -0.5f : 0.5f)); + g = static_cast(exponent); + g = (x - g * Ln2High) - g * Ln2Low; + float factor; + if (-Eps < g && g < Eps) { + factor = 1.0f; + } else { + const float z = g * g; + const float w = Q * z + 1.0f; + g *= P0 + P1 * z; + factor = (w + g) / (w - g) * 2.0f; + --exponent; + } + const short result = ScaleProduct(*value, factor, y, exponent + offset); + if (result == Finite && std::fpclassify(*value) == FP_SUBNORMAL) { + std::fesetexceptflag(&entryFlags, FE_ALL_EXCEPT); + errno = entryErrno; + } + return result; +} + double APS5_VABI _Sin_nid_postfix(double x, unsigned int quadrant) { return quadrant != 0 ? cos_nid_postfix(x) : sin_nid_postfix(x); } @@ -143,6 +402,22 @@ long double APS5_VABI _LSin_nid_postfix(long double x, unsigned int quadrant) { return quadrant != 0 ? cosl_nid_postfix(x) : sinl_nid_postfix(x); } +double APS5_VABI _Divide_nid_postfix(double x, double y) { + return x / y; +} + +float APS5_VABI _FDivide_nid_postfix(float x, float y) { + return x / y; +} + +double APS5_VABI _Recip_nid_postfix(double x) { + return 1.0 / x; +} + +float APS5_VABI _FRecip_nid_postfix(float x) { + return 1.0f / x; +} + double APS5_VABI _Log_nid_postfix(double x, int base) { if (base > 0) return log10_nid_postfix(x); return base == 0 ? log_nid_postfix(x) : log2_nid_postfix(x); @@ -153,4 +428,17 @@ float APS5_VABI _FLog_nid_postfix(float x, int base) { return base == 0 ? logf_nid_postfix(x) : log2f_nid_postfix(x); } +long double APS5_VABI _LLog_nid_postfix(long double x, int base) { + if (base > 0) return log10l_nid_postfix(x); + return base == 0 ? logl_nid_postfix(x) : log2l_nid_postfix(x); +} + +long double APS5_VABI _LDivide_nid_postfix(long double x, long double y) { + return x / y; +} + +long double APS5_VABI _LRecip_nid_postfix(long double x) { + return 1.0L / x; +} + } diff --git a/core/libs/prx/libc/src/Directory.cpp b/core/libs/prx/libc/src/Directory.cpp index 69d3670e80..63848ea167 100644 --- a/core/libs/prx/libc/src/Directory.cpp +++ b/core/libs/prx/libc/src/Directory.cpp @@ -1,3 +1,4 @@ +#include "prx/libc/include/GuestFileNumber.hpp" #include "prx/libc/include/Api.hpp" #include "prx/libc/include/GuestDirectory.hpp" #include "prx/libc/include/General.hpp" @@ -5,11 +6,14 @@ #include "prx/libc/include/AppArchive.hpp" #include #include +#include +#include #include #include #include #include #include +#include #include #ifdef _WIN32 #include "prx/libc/include/WindowsDescriptor.hpp" @@ -34,6 +38,13 @@ int DirectoryError(int error) { default: return 5; } } +int DirectoryError(const std::error_code& error) { +#ifdef _WIN32 + if (error.category() == std::system_category()) + return FilesystemError(std::error_code(WindowsOpenError_nid_no_patch(static_cast(error.value())), std::generic_category())); +#endif + return FilesystemError(error); +} #ifdef _WIN32 struct NativeDirectory { HANDLE handle = INVALID_HANDLE_VALUE; @@ -54,7 +65,16 @@ int NativeOpenError(DWORD error) { NativeDirectory* OpenNative(const std::filesystem::path& path) { const DWORD attributes = GetFileAttributesW(path.c_str()); if (attributes == INVALID_FILE_ATTRIBUTES) { errno = NativeOpenError(GetLastError()); return nullptr; } - if ((attributes & FILE_ATTRIBUTE_DIRECTORY) == 0) { errno = ENOTDIR; 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, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (followed != INVALID_HANDLE_VALUE) CloseHandle(followed); + else if (GetLastError() == ERROR_CANT_RESOLVE_FILENAME) errno = ELOOP; + } + 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, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); @@ -153,9 +173,17 @@ struct Directory { NativeDirectory* native = nullptr; std::filesystem::path path; GuestDirectoryEntry entry{}; + std::vector entries; + std::vector pending; + std::size_t cursor = 0; + std::size_t pendingCursor = 0; + bool indexed = false; + bool descriptorDirectory = false; std::mutex mutex; int descriptor = -1; int (APS5_VABI* closeDescriptor)(int) = nullptr; + int (APS5_VABI* readDescriptor)(int, char*, int) = nullptr; + std::int64_t (APS5_VABI* seekDescriptor)(int, std::int64_t, int) = nullptr; ~Directory() { if (native) CloseNative(native); } }; std::uint8_t DirectoryType(const std::filesystem::path& path) { @@ -173,6 +201,37 @@ std::uint8_t DirectoryType(const std::filesystem::path& path) { } GuestDirectoryEntry* ReadEntry(Directory& directory) { const int savedError = errno; + if (directory.descriptorDirectory) { + if (directory.pendingCursor >= directory.pending.size()) { + directory.pending.clear(); + directory.pendingCursor = 0; + std::array bytes{}; + const int read = directory.readDescriptor(directory.descriptor, bytes.data(), static_cast(bytes.size())); + if (read < 0) { + errno = static_cast(static_cast(read) & 0xffffu); + return nullptr; + } + if (read == 0) { errno = savedError; return nullptr; } + for (int offset = 0; offset < read;) { + GuestDirectoryEntry entry{}; + std::uint16_t recordLength = 0; + std::memcpy(&recordLength, bytes.data() + offset + 4, sizeof(recordLength)); + if (recordLength < 12 || offset + recordLength > read || recordLength > sizeof(entry)) { errno = 5; return nullptr; } + std::memcpy(&entry, bytes.data() + offset, recordLength); + directory.pending.push_back(entry); + offset += recordLength; + } + } + directory.entry = directory.pending[directory.pendingCursor++]; + errno = savedError; + return &directory.entry; + } + if (directory.indexed) { + if (directory.cursor >= directory.entries.size()) { errno = savedError; return nullptr; } + directory.entry = directory.entries[directory.cursor++]; + errno = savedError; + return &directory.entry; + } errno = 0; std::filesystem::path component; std::string name; @@ -195,7 +254,67 @@ GuestDirectoryEntry* ReadEntry(Directory& directory) { errno = savedError; return &directory.entry; } catch (const std::bad_alloc&) { errno = 12; return nullptr; } - catch (const std::filesystem::filesystem_error&) { errno = 5; return nullptr; } + catch (const std::system_error& error) { errno = DirectoryError(error.code()); return nullptr; } + catch (const std::exception&) { errno = 5; return nullptr; } +} + +GuestDirectoryEntry MakeEntry(const std::string& name, std::uint64_t inode, std::uint8_t type) { + if (name.size() > 255) throw std::system_error(ENAMETOOLONG, std::generic_category(), "directory entry name"); + GuestDirectoryEntry entry{}; + entry.fileNumber = GuestFileNumber_nid_no_patch(inode); + entry.recordLength = static_cast(8 + ((name.size() + 1 + 3) & ~std::size_t{3})); + entry.nameLength = static_cast(name.size()); + entry.type = type; + std::memcpy(entry.name, name.c_str(), name.size() + 1); + return entry; +} + +void OpenIndexed(Directory& directory) { + directory.indexed = true; + std::vector names; + const auto archiveOnly = AppArchiveOnly_nid_no_patch(directory.path); + if (!archiveOnly) { + directory.native = OpenNative(directory.path); + if (!directory.native) throw std::system_error(errno, std::generic_category(), "open merged directory"); + for (;;) { + errno = 0; + std::filesystem::path component; + std::string name; + std::uint64_t inode = 0; + if (!ReadNative(directory.native, component, name, inode)) { + if (errno != 0) throw std::system_error(errno, std::generic_category(), "read merged directory"); + break; + } + names.push_back(name); + directory.entries.push_back(MakeEntry(name, inode, DirectoryType(directory.path / component))); + } + CloseNative(directory.native); + directory.native = nullptr; + } + AppArchiveNode node{}; + if (!AppArchiveLookup_nid_no_patch(directory.path, &node) || !node.Directory) + throw std::logic_error("OpenIndexed: app0.zar directory disappeared"); + const auto hasName = [&names](const std::string& name) { + return std::any_of(names.begin(), names.end(), [&name](const std::string& existing) { + return AppArchiveNameMatches_nid_no_patch(existing, name); + }); + }; + if (!hasName(".")) { + const auto dotNumber = archiveOnly ? static_cast(node.Handle) + 1 : 0; + directory.entries.push_back(MakeEntry(".", dotNumber, 4)); + names.push_back("."); + } + if (!hasName("..")) { + AppArchiveNode parent{}; + const auto parentPath = directory.path.parent_path(); + const auto parentNumber = AppArchiveLookup_nid_no_patch(parentPath, &parent) && parent.Directory ? static_cast(parent.Handle) + 1 : 0; + directory.entries.push_back(MakeEntry("..", parentNumber, 4)); + names.push_back(".."); + } + const auto archiveEntries = archiveOnly ? AppArchiveListDirectory_nid_no_patch(directory.path) + : AppArchiveListUnshadowedDirectory_nid_no_patch(directory.path, names); + for (const auto& entry : archiveEntries) + directory.entries.push_back(MakeEntry(entry.Name, static_cast(entry.Handle) + 1, entry.Directory ? 4 : 8)); } } @@ -205,17 +324,21 @@ void* APS5_VABI opendir_nid_postfix(const char* path) { if (!*path) { errno = 2; return nullptr; } try { auto directory = std::make_unique(); - if (const int error = ResolvePathError_nid_no_patch(path, directory->path)) { errno = error; return nullptr; } - AppArchiveRefuseMergedListing_nid_no_patch(directory->path, path); - directory->native = OpenNative(directory->path); - if (!directory->native) { - errno = DirectoryError(errno); - if (errno == 2) errno = NotFoundError_nid_no_patch(directory->path); - return nullptr; + if (const int error = ResolvePathError_nid_no_patch(path, directory->path, false, true)) { errno = error; return nullptr; } + if (AppArchiveHasDirectory_nid_no_patch(directory->path)) { + OpenIndexed(*directory); + } else { + directory->native = OpenNative(directory->path); + if (!directory->native) { + errno = DirectoryError(errno); + if (errno == 2) errno = NotFoundError_nid_no_patch(directory->path); + return nullptr; + } } return directory.release(); } catch (const std::bad_alloc&) { errno = 12; return nullptr; } - catch (const std::filesystem::filesystem_error&) { errno = 5; return nullptr; } + catch (const std::system_error& error) { errno = DirectoryError(error.code()); return nullptr; } + catch (const std::exception&) { errno = 5; return nullptr; } } GuestDirectoryEntry* APS5_VABI readdir_nid_postfix(void* handle) { @@ -246,7 +369,7 @@ int APS5_VABI readdir_r_nid_postfix(void* handle, GuestDirectoryEntry* entry, Gu int APS5_VABI closedir_nid_postfix(void* handle) { if (!handle) { errno = 9; return -1; } std::unique_ptr directory(static_cast(handle)); - int result = CloseNative(directory->native); + int result = directory->native ? CloseNative(directory->native) : 0; directory->native = nullptr; if (result) errno = DirectoryError(errno); if (directory->closeDescriptor && directory->closeDescriptor(directory->descriptor) != 0) result = -1; @@ -257,27 +380,76 @@ void APS5_VABI rewinddir_nid_postfix(void* handle) { if (!handle) { errno = 9; return; } auto& directory = *static_cast(handle); std::lock_guard lock(directory.mutex); - RewindNative(directory.native); + if (directory.descriptorDirectory) { + if (directory.seekDescriptor(directory.descriptor, 0, 0) >= 0) { + directory.pending.clear(); + directory.pendingCursor = 0; + } + } else if (directory.indexed) { + auto entries = std::move(directory.entries); + const auto cursor = directory.cursor; + const auto indexed = directory.indexed; + directory.entries.clear(); + directory.cursor = 0; + directory.indexed = false; + const auto restore = [&directory, &entries, cursor, indexed] { + if (directory.native) { + CloseNative(directory.native); + directory.native = nullptr; + } + directory.entries = std::move(entries); + directory.cursor = cursor; + directory.indexed = indexed; + }; + try { OpenIndexed(directory); } + catch (const std::bad_alloc&) { restore(); errno = 12; } + catch (const std::system_error& error) { restore(); errno = DirectoryError(error.code()); } + catch (const std::exception&) { restore(); errno = 5; } + } + else RewindNative(directory.native); } std::int64_t APS5_VABI telldir_nid_postfix(void* handle) { if (!handle) { errno = 9; return -1; } auto& directory = *static_cast(handle); std::lock_guard lock(directory.mutex); - return TellNative(directory.native); + if (directory.descriptorDirectory) { + const auto position = directory.seekDescriptor(directory.descriptor, 0, 1); + if (position < 0) return position; + const auto unread = directory.pending.size() - directory.pendingCursor; + return position >= static_cast(unread) ? position - static_cast(unread) : 0; + } + return directory.indexed ? static_cast(directory.cursor) : TellNative(directory.native); } void APS5_VABI seekdir_nid_postfix(void* handle, std::int64_t location) { if (!handle) { errno = 9; return; } auto& directory = *static_cast(handle); std::lock_guard lock(directory.mutex); - SeekNative(directory.native, static_cast(location)); + if (directory.descriptorDirectory) { + if (directory.seekDescriptor(directory.descriptor, location, 0) >= 0) { + directory.pending.clear(); + directory.pendingCursor = 0; + } + } else if (directory.indexed) { + if (location >= 0 && static_cast(location) <= directory.entries.size()) directory.cursor = static_cast(location); + else errno = 22; + } else SeekNative(directory.native, static_cast(location)); } -void* APS5_VABI OpenDescriptorDirectory_nid_no_patch(const char* hostPath, int descriptor, int (APS5_VABI* closeDescriptor)(int), std::uint64_t position) { +void* APS5_VABI OpenDescriptorDirectory_nid_no_patch(const char* hostPath, int descriptor, int (APS5_VABI* closeDescriptor)(int), std::uint64_t position, + int (APS5_VABI* readDescriptor)(int, char*, int), std::int64_t (APS5_VABI* seekDescriptor)(int, std::int64_t, int)) { try { auto directory = std::make_unique(); directory->path = std::filesystem::path(reinterpret_cast(hostPath)); + if (readDescriptor && seekDescriptor) { + directory->descriptor = descriptor; + directory->closeDescriptor = closeDescriptor; + directory->readDescriptor = readDescriptor; + directory->seekDescriptor = seekDescriptor; + directory->descriptorDirectory = true; + return directory.release(); + } #ifdef _WIN32 directory->native = OpenNative(directory->path); if (!directory->native) { errno = DirectoryError(errno); return nullptr; } @@ -298,6 +470,6 @@ void* APS5_VABI OpenDescriptorDirectory_nid_no_patch(const char* hostPath, int d directory->closeDescriptor = closeDescriptor; return directory.release(); } catch (const std::bad_alloc&) { errno = 12; return nullptr; } - catch (const std::filesystem::filesystem_error&) { errno = 5; return nullptr; } + catch (const std::system_error& error) { errno = DirectoryError(error.code()); return nullptr; } } } diff --git a/core/libs/prx/libc/src/Errors.cpp b/core/libs/prx/libc/src/Errors.cpp index 04d0187df9..4d98ef0197 100644 --- a/core/libs/prx/libc/src/Errors.cpp +++ b/core/libs/prx/libc/src/Errors.cpp @@ -4,6 +4,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include namespace { // Guest FreeBSD errno numbering, not the host CRT's numbering. @@ -43,34 +44,32 @@ constexpr const char* messages[] = { "Previous owner died" }; static_assert(sizeof(messages) / sizeof(messages[0]) == 97); -bool Known(int error) { return error >= 0 && error < 97; } bool NamedByStrerror(int error) { return error > 0 && error < 95; } + +void Describe(int error, char (&text)[128]) { + if (error == 0) std::snprintf(text, sizeof(text), "No error"); + else if (NamedByStrerror(error)) std::snprintf(text, sizeof(text), "%s", messages[error]); + else std::snprintf(text, sizeof(text), "error_code=0x%08x", static_cast(error)); +} } extern "C" { char* APS5_VABI strerror_nid_postfix(int error) { thread_local char buffer[128]; - if (error == 0) std::snprintf(buffer, sizeof(buffer), "No error"); - else if (NamedByStrerror(error)) std::snprintf(buffer, sizeof(buffer), "%s", messages[error]); - else std::snprintf(buffer, sizeof(buffer), "error_code=0x%08x", static_cast(error)); + Describe(error, buffer); return buffer; } int APS5_VABI strerror_r_nid_postfix(int error, char* buffer, std::size_t length) { + if (length < 23) return 34; + if (!buffer) throw std::invalid_argument("strerror_r: a null buffer of 23 bytes or more, which the console writes through"); const int saved = *__error_nid_postfix(); - char temporary[128]; - if (Known(error)) std::snprintf(temporary, sizeof(temporary), "%s", messages[error]); - else std::snprintf(temporary, sizeof(temporary), "Unknown error: %d", error); - const auto required = std::strlen(temporary) + 1; - int result = Known(error) ? 0 : 22; - if (Known(error) && (!buffer || length < required)) result = 34; - if (buffer && length) { - const auto count = required <= length ? required - 1 : length - 1; - std::memcpy(buffer, temporary, count); - buffer[count] = '\0'; - } + char text[128]; + Describe(error, text); *__error_nid_postfix() = saved; - return result; + std::strncpy(buffer, text, length - 1); + buffer[length - 1] = '\0'; + return 0; } void APS5_VABI perror_nid_postfix(const char* prefix) { diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index d50cdfad16..e023b0eaf4 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -302,11 +302,11 @@ void* heapAllocation(std::size_t bytes, TAllocate allocate) { extern "C" { FileStream* APS5_VABI fdopen_nid_postfix(int descriptor, const char* mode) { - if (descriptor < 0) { errno = 9; return nullptr; } if (!mode) { errno = 22; return nullptr; } - AppArchiveRefuseDescriptor_nid_no_patch(descriptor, "fdopen"); const auto parsed = ParseFileMode(mode); if (!parsed) { errno = 22; return nullptr; } + if (descriptor < 0) return nullptr; + AppArchiveRefuseDescriptor_nid_no_patch(descriptor, "fdopen"); try { auto nativeMode = parsed->native; std::erase(nativeMode, 'x'); @@ -315,9 +315,6 @@ FileStream* APS5_VABI fdopen_nid_postfix(int descriptor, const char* mode) { #ifdef _WIN32 const auto handle = WindowsDescriptorHandle_nid_no_patch(descriptor); if (handle == INVALID_HANDLE_VALUE) { errno = 9; return nullptr; } - const int access = WindowsDescriptorAccess_nid_no_patch(handle, descriptor, "fdopen"); - const int requested = nativeMode.find('+') != std::string::npos ? 2 : nativeMode[0] == 'r' ? 0 : 1; - if (access != 2 && access != requested) { errno = 22; return nullptr; } if (parsed->append && !AppendThroughDescriptor(descriptor)) return nullptr; auto* native = ::_fdopen(descriptor, nativeMode.c_str()); #else @@ -465,6 +462,15 @@ int APS5_VABI fopen_s_nid_postfix(FileStream** result, const char* filename, con return *result ? 0 : errno; } +int APS5_VABI freopen_s_nid_postfix(FileStream** result, const char* filename, const char* mode, FileStream* stream) { + constexpr int GuestEinval = 22; + if (!result) return GuestEinval; + *result = nullptr; + if (!mode || !stream) return GuestEinval; + *result = freopen_nid_postfix(filename, mode, stream); + return *result ? 0 : errno; +} + int APS5_VABI fclose_nid_postfix(FileStream* stream) { if (stream && !stream->IsOpen()) { std::unique_ptr owner(stream->IsDynamic() ? stream : nullptr); diff --git a/core/libs/prx/libc/src/Filesystem.cpp b/core/libs/prx/libc/src/Filesystem.cpp index 617ebf6e63..b6c54e3d2d 100644 --- a/core/libs/prx/libc/src/Filesystem.cpp +++ b/core/libs/prx/libc/src/Filesystem.cpp @@ -12,6 +12,30 @@ #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, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) return {}; + std::wstring target(MAX_PATH, L'\0'); + DWORD length = GetFinalPathNameByHandleW(handle, target.data(), static_cast(target.size()), FILE_NAME_NORMALIZED); + if (length >= target.size()) { + target.resize(length); + length = GetFinalPathNameByHandleW(handle, target.data(), length, FILE_NAME_NORMALIZED); + } + const DWORD error = GetLastError(); + CloseHandle(handle); + if (length == 0 || length >= target.size()) { + SetLastError(length == 0 ? error : ERROR_FILENAME_EXCED_RANGE); + return {}; + } + target.resize(length); + return target; +} +#endif + extern "C" int APS5_VABI access_nid_postfix(const char* path, int mode) { if (mode < 0 || (mode & ~7)) { errno = 22; return -1; } if (!path) { errno = 14; return -1; } @@ -19,9 +43,11 @@ extern "C" int APS5_VABI access_nid_postfix(const char* path, int mode) { try { const auto resolved = ResolvePath_nid_no_patch(path); #ifdef _WIN32 - const DWORD attributes = GetFileAttributesW(resolved.c_str()); + const auto target = FollowedPath(resolved); + const DWORD attributes = target.empty() ? INVALID_FILE_ATTRIBUTES : GetFileAttributesW(target.c_str()); if (attributes == INVALID_FILE_ATTRIBUTES) { - errno = FilesystemError(std::error_code(static_cast(GetLastError()), std::system_category())); + const DWORD failure = GetLastError(); + errno = failure == ERROR_CANT_RESOLVE_FILENAME ? 62 : FilesystemError(std::error_code(static_cast(failure), std::system_category())); if (errno == 2) errno = NotFoundError_nid_no_patch(resolved); return -1; } @@ -31,10 +57,10 @@ extern "C" int APS5_VABI access_nid_postfix(const char* path, int mode) { } DWORD size = 0; constexpr auto information = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION; - GetFileSecurityW(resolved.c_str(), information, nullptr, 0, &size); + GetFileSecurityW(target.c_str(), information, nullptr, 0, &size); if (!size) { errno = 13; return -1; } std::vector descriptor(size); - if (!GetFileSecurityW(resolved.c_str(), information, descriptor.data(), size, &size)) { + if (!GetFileSecurityW(target.c_str(), information, descriptor.data(), size, &size)) { errno = 13; return -1; } struct Token { @@ -72,6 +98,39 @@ extern "C" int APS5_VABI access_nid_postfix(const char* path, int mode) { } } +extern "C" bool ExecutePermission_nid_no_patch(const std::filesystem::path& native) { +#ifdef _WIN32 + const auto target = FollowedPath(native); + if (target.empty() || GetFileAttributesW(target.c_str()) == INVALID_FILE_ATTRIBUTES) return false; + DWORD size = 0; + constexpr auto information = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION; + GetFileSecurityW(target.c_str(), information, nullptr, 0, &size); + if (!size) return false; + std::vector descriptor(size); + if (!GetFileSecurityW(target.c_str(), information, descriptor.data(), size, &size)) return false; + struct Token { + HANDLE value = nullptr; + ~Token() { if (value) CloseHandle(value); } + } source, impersonation; + if (!OpenThreadToken(GetCurrentThread(), TOKEN_QUERY | TOKEN_DUPLICATE, TRUE, &source.value)) { + if (GetLastError() != ERROR_NO_TOKEN || + !OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_DUPLICATE, &source.value)) { + return false; + } + } + if (!DuplicateToken(source.value, SecurityImpersonation, &impersonation.value)) return false; + GENERIC_MAPPING mapping{FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_GENERIC_EXECUTE, FILE_ALL_ACCESS}; + PRIVILEGE_SET privileges{}; + DWORD privilegeSize = sizeof(privileges), granted = 0; + BOOL allowed = FALSE; + if (!AccessCheck(descriptor.data(), impersonation.value, FILE_EXECUTE, &mapping, + &privileges, &privilegeSize, &granted, &allowed) || !allowed) return false; + return true; +#else + return ::access(native.c_str(), X_OK) == 0; +#endif +} + extern "C" int APS5_VABI remove_nid_postfix(const char* path) { if (!path) { errno = 14; return -1; } if (!*path) { errno = 2; return -1; } diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index d0066ae761..a67a0b877e 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -45,6 +45,7 @@ bool RequiresGuestFormatting(const char* format) { (*format == 'l' && (format[1] == 's' || format[1] == 'c' || format[1] == 'p'))) return true; while (*format && std::strchr("hljztL", *format)) ++format; if (*format && !std::strchr(LibcDetail::KnownConversions, *format)) return true; + if (*format && std::strchr("aAeEfFgG", *format)) return true; if (*format) ++format; } return false; @@ -124,10 +125,69 @@ int HostVfscanf(std::FILE* stream, const char* format, std::va_list args) { } #endif -template -int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer nextPointer, NextCapacity nextCapacity) { - if (buffer == nullptr || format == nullptr) return EOF; - const char* input = buffer; +int HostFscanf(std::FILE* stream, const char* format, ...) { + std::va_list args; + va_start(args, format); +#ifdef _WIN32 + const int result = std::vfscanf(stream, format, args); +#else + const int result = HostVfscanf(stream, format, args); +#endif + va_end(args); + return result; +} + +class StringSource { + const char* const start; + const char* position; + +public: + static constexpr bool Contiguous = true; + + explicit StringSource(const char* text) : start(text), position(text) {} + + int Peek() const { return *position == '\0' ? EOF : static_cast(*position); } + void Skip() { ++position; } + void Advance(std::ptrdiff_t count) { position += count; } + std::ptrdiff_t Consumed() const { return position - start; } + const char* Remaining() const { return position; } + + int Scan(const char* directive, bool suppress, void* target, int* consumed) const { + return suppress ? HostSscanf(position, directive, consumed) : HostSscanf(position, directive, target, consumed); + } +}; + +class StreamSource { + std::FILE* const file; + std::ptrdiff_t consumed = 0; + +public: + static constexpr bool Contiguous = false; + + explicit StreamSource(std::FILE* native) : file(native) {} + + int Peek() const { + const int next = std::fgetc(file); + if (next != EOF) std::ungetc(next, file); + return next; + } + + void Skip() { + std::fgetc(file); + ++consumed; + } + + void Advance(std::ptrdiff_t count) { consumed += count; } + std::ptrdiff_t Consumed() const { return consumed; } + + int Scan(const char* directive, bool suppress, void* target, int* count) const { + return suppress ? HostFscanf(file, directive, count) : HostFscanf(file, directive, target, count); + } +}; + +template +int ScanSource(TSource source, const char* format, bool secure, NextPointer nextPointer, NextCapacity nextCapacity) { + if (format == nullptr) return EOF; int assigned = 0; bool converted = false; const char* cursor = format; @@ -135,15 +195,15 @@ int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer n while (*cursor != '\0') { if (std::isspace(static_cast(*cursor))) { while (std::isspace(static_cast(*cursor))) ++cursor; - while (std::isspace(static_cast(*input))) ++input; + while (std::isspace(source.Peek())) source.Skip(); continue; } if (*cursor != '%' || cursor[1] == '%') { const char expected = *cursor == '%' ? '%' : *cursor; cursor += *cursor == '%' ? 2 : 1; - if (expected == '%') while (std::isspace(static_cast(*input))) ++input; - if (*input != expected) return finish(!converted && *input == '\0' ? EOF : assigned); - ++input; + if (expected == '%') while (std::isspace(source.Peek())) source.Skip(); + if (source.Peek() != static_cast(expected)) return finish(!converted && source.Peek() == EOF ? EOF : assigned); + source.Skip(); continue; } ++cursor; @@ -170,7 +230,7 @@ int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer n if (!suppress) { void* target = nextPointer(); if (target == nullptr) return EOF; - const auto count = input - buffer; + const auto count = source.Consumed(); if (length == "hh") *static_cast(target) = static_cast(count); else if (length == "h") *static_cast(target) = static_cast(count); else if (length.empty()) *static_cast(target) = static_cast(count); @@ -182,7 +242,8 @@ int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer n continue; } #ifndef _WIN32 - if (IsWideScan(length, conversion)) { + 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 (kind == 's') { @@ -228,15 +289,14 @@ int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer n if (kind != 'c') target[text.size()] = u'\0'; ++assigned; } - input = field; + source.Advance(field - input); converted = true; continue; } #endif if (conversion != 'c' && conversion != '[') { - const char* probe = input; - while (std::isspace(static_cast(*probe))) ++probe; - if (*probe == '\0') return finish(converted ? assigned : EOF); + while (std::isspace(source.Peek())) source.Skip(); + if (source.Peek() == EOF) return finish(converted ? assigned : EOF); } const bool sized = secure && (conversion == 's' || conversion == 'c' || conversion == '['); bool capped = false; @@ -263,29 +323,50 @@ int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer n directive += specifier; directive += "%n"; int consumed = -1; - const int matched = suppress ? HostSscanf(input, directive.c_str(), &consumed) : HostSscanf(input, directive.c_str(), target, &consumed); - if (consumed < 0 || (!suppress && matched != 1)) return finish(!converted && *input == '\0' ? EOF : assigned); + const int matched = source.Scan(directive.c_str(), suppress, target, &consumed); + if (consumed < 0 || (!suppress && matched != 1)) return finish(!converted && source.Peek() == EOF ? EOF : assigned); + source.Advance(consumed); if (capped && conversion != 'c') { - const char next = input[consumed]; + const int next = source.Peek(); bool overflow = false; - if (conversion == 's') overflow = next != '\0' && !std::isspace(static_cast(next)); - else if (next != '\0') { + if (conversion == 's') overflow = next != EOF && !std::isspace(next); + else if (next != EOF) { char probe[2]; + const char text[2] = {static_cast(next), '\0'}; const std::string test = "%1" + specifier; - overflow = std::sscanf(input + consumed, test.c_str(), probe) == 1; + overflow = std::sscanf(text, test.c_str(), probe) == 1; } if (overflow) { static_cast(target)[0] = '\0'; return finish(assigned); } } - input += consumed; converted = true; if (!suppress) ++assigned; } return finish(assigned); } +template +int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer nextPointer, NextCapacity nextCapacity) { + if (buffer == nullptr) return EOF; + return ScanSource(StringSource(buffer), format, secure, nextPointer, nextCapacity); +} + +template +int ScanGuestStream(FileStream* stream, const char* format, NextPointer nextPointer, NextCapacity nextCapacity) { + if (stream == nullptr || format == nullptr) return EOF; + if (!stream->IsOpen()) { errno = 9; return EOF; } + auto* native = GetNativeStream(stream); + if (!stream->SwitchToReading()) return EOF; +#ifndef _WIN32 + RejectGuestWideScan(format); +#endif + const int result = ScanSource(StreamSource(native), format, true, nextPointer, nextCapacity); + stream->SyncStatus(); + return result; +} + } extern "C" { @@ -576,6 +657,47 @@ int APS5_VABI vsnprintf_s_nid_postfix(char* buffer, size_t size, const char* for return FormatSecure(buffer, size, format, args); } +static int WriteSecure(FileStream* stream, const char* format, const void* args) { + if (stream == nullptr || format == nullptr || args == nullptr) return -1; + if (!stream->IsOpen()) { errno = 9; return -1; } + auto* native = GetNativeStream(stream); + std::string text; + int count; + try { + count = LibcDetail::FormatWindows(nullptr, 0, format, args, &text, true); + } catch (const LibcDetail::FormatConstraintViolation&) { + return -1; + } + const int result = std::fwrite(text.data(), 1, static_cast(count), native) == static_cast(count) ? count : -1; + stream->SyncStatus(); + return result; +} + +int APS5_VABI vfprintf_s_nid_postfix(FileStream* stream, const char* format, VaList* args) { + return WriteSecure(stream, format, args); +} + +int APS5_VABI vprintf_s_nid_postfix(const char* format, VaList* args) { + return WriteSecure(&_Stdout_nid_postfix, format, args); +} + +int APS5_VABI fprintf_s_nid_postfix(FileStream* stream, const char* format, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); +#else + std::va_list args; + va_start(args, format); +#endif + const int result = vfprintf_s_nid_postfix(stream, format, reinterpret_cast(args)); +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif + return result; +} + #ifdef _WIN32 int APS5_VABI vsscanf_s_nid_postfix(const char* buffer, const char* format, VaList* args) { @@ -653,6 +775,7 @@ extern FileStream* __stdinp_nid_postfix; int APS5_VABI vfscanf_nid_postfix(FileStream* stream, const char* format, VaList* args) { if (stream && !stream->IsOpen()) { errno = 9; return -1; } auto* native = GetNativeStream(stream); + if (!stream->SwitchToReading()) return EOF; #ifdef _WIN32 const int result = LibcDetail::ScanFileWindows_nid_no_patch(native, format, args); #else @@ -688,4 +811,47 @@ int APS5_VABI scanf_nid_postfix(const char* format, ...) { return result; } +int APS5_VABI vfscanf_s_nid_postfix(FileStream* stream, const char* format, VaList* args) { + LibcDetail::FormatArguments arguments(args); + return ScanGuestStream(stream, format, [&] { return arguments.Next(); }, [&] { return arguments.Next(); }); +} + +int APS5_VABI vscanf_s_nid_postfix(const char* format, VaList* args) { + return vfscanf_s_nid_postfix(__stdinp_nid_postfix, format, args); +} + +int APS5_VABI fscanf_s_nid_postfix(FileStream* stream, const char* format, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); +#else + std::va_list args; + va_start(args, format); +#endif + const int result = vfscanf_s_nid_postfix(stream, format, reinterpret_cast(args)); +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif + return result; +} + +int APS5_VABI scanf_s_nid_postfix(const char* format, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); +#else + std::va_list args; + va_start(args, format); +#endif + const int result = vfscanf_s_nid_postfix(__stdinp_nid_postfix, format, reinterpret_cast(args)); +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif + return result; +} + } diff --git a/core/libs/prx/libc/src/FormattingWide.cpp b/core/libs/prx/libc/src/FormattingWide.cpp index 66fd9d1142..0794a9db66 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/Api.hpp" #include #include +#include #include #include #include @@ -82,6 +83,18 @@ template void AppendNumber(std::u16string& out, const std::string& out.append(text.begin(), text.begin() + size); } +std::u16string WideNanText(const std::string& spec, bool negative, bool upper) { + char16_t sign = negative ? u'-' : u'\0'; + for (std::size_t index = 1; !negative && index < spec.size() && std::strchr("-+ #0", spec[index]); ++index) { + if (spec[index] == '+') sign = u'+'; + else if (spec[index] == ' ' && sign == u'\0') sign = u' '; + } + std::u16string text; + if (sign != u'\0') text.push_back(sign); + text += upper ? u"NAN" : u"nan"; + return text; +} + void AppendPadded(std::u16string& out, const std::u16string& text, bool left, int width) { const std::size_t pad = width > 0 && static_cast(width) > text.size() ? static_cast(width) - text.size() : 0; if (!left) out.append(pad, u' '); @@ -172,10 +185,29 @@ std::u16string FormatWide(const char16_t* format, VaList* source, bool secure = spec = spec.substr(0, spec.find('.')) + ".1"; AppendNumber(out, spec + "ll" + static_cast(conversion), value); } else if (In(conversion, "aAeEfFgG")) { - if (length == "L") AppendNumber(out, spec + "L" + static_cast(conversion), args.Next()); - else { + const char narrow = static_cast(conversion); + const bool hexadecimal = narrow == 'a' || narrow == 'A'; + if (length == "L") { + const long double value = args.Next(); + if (std::isnan(value)) { + AppendPadded(out, WideNanText(spec, std::signbit(value), conversion < u'a'), left, width); + } else if (std::isfinite(value)) { + const std::string text = hexadecimal ? LibcDetail::HexFloat(spec, narrow, value) : LibcDetail::DecimalFloat(spec, narrow, value); + out.append(text.begin(), text.end()); + } else { + AppendNumber(out, spec + "L" + narrow, value); + } + } else { if (!length.empty() && length != "l") throw std::invalid_argument("Invalid floating length"); - AppendNumber(out, spec + static_cast(conversion), args.Next()); + const double value = args.Next(); + if (std::isnan(value)) { + AppendPadded(out, WideNanText(spec, std::signbit(value), conversion < u'a'), left, width); + } else if (std::isfinite(value)) { + const std::string text = hexadecimal ? LibcDetail::HexFloat(spec, narrow, value) : LibcDetail::DecimalFloat(spec, narrow, value); + out.append(text.begin(), text.end()); + } else { + AppendNumber(out, spec + narrow, value); + } } } else if (conversion == u'p' && length.empty()) { const std::string text = LibcDetail::FormatPointer(spec, args.Next()); @@ -321,6 +353,7 @@ int APS5_VABI vsnwprintf_s_nid_postfix(char16_t* buffer, std::size_t size, const } int APS5_VABI wprintf_nid_postfix(const char16_t* format, ...) { + if (!__stdoutp_nid_postfix->IsOpen()) { errno = 9; return -1; } APS5_VA_BEGIN(format); int result = -1; try { @@ -360,8 +393,42 @@ int APS5_VABI vwprintf_nid_postfix(const char16_t* format, VaList* args) { return vfwprintf_nid_postfix(__stdoutp_nid_postfix, format, args); } +int APS5_VABI vfwprintf_s_nid_postfix(FileStream* stream, const char16_t* format, VaList* args) { + if (stream == nullptr || format == nullptr || args == nullptr) return -1; + if (!stream->IsOpen()) { errno = 9; return -1; } + int result = -1; + try { + const std::u16string text = FormatWide(format, args, true); + const std::string bytes = ToUtf8(text); + if (text.size() <= static_cast(INT_MAX) && + std::fwrite(bytes.data(), 1, bytes.size(), GetNativeStream(stream)) == bytes.size()) result = static_cast(text.size()); + } catch (const std::invalid_argument&) { + } + stream->SyncStatus(); + return result; +} + +int APS5_VABI vwprintf_s_nid_postfix(const char16_t* format, VaList* args) { + return vfwprintf_s_nid_postfix(__stdoutp_nid_postfix, format, args); +} + +int APS5_VABI fwprintf_s_nid_postfix(FileStream* stream, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfwprintf_s_nid_postfix(stream, format, reinterpret_cast(args)); + APS5_VA_END(); + return result; +} + +int APS5_VABI wprintf_s_nid_postfix(const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfwprintf_s_nid_postfix(__stdoutp_nid_postfix, format, reinterpret_cast(args)); + APS5_VA_END(); + return result; +} + int APS5_VABI fputwc_nid_postfix(char16_t value, FileStream* stream) { if (stream == nullptr) { errno = 22; return -1; } + if (!stream->IsOpen()) { errno = 9; return -1; } try { const std::string bytes = ToUtf8(std::u16string(1, value)); auto* native = GetNativeStream(stream); @@ -376,6 +443,7 @@ int APS5_VABI fputwc_nid_postfix(char16_t value, FileStream* stream) { int APS5_VABI fputws_nid_postfix(const char16_t* str, FileStream* stream) { if (str == nullptr || stream == nullptr) { errno = 22; return -1; } + if (!stream->IsOpen()) { errno = 9; return -1; } try { const std::u16string text(str); const std::string bytes = ToUtf8(text); diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 73c1a10082..ecd6abc0c9 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -1,5 +1,7 @@ #include "prx/libc/include/Api.hpp" +#include #include +#include #include #include #include @@ -8,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -29,12 +32,26 @@ #include #include #include +#ifdef __linux__ +#include +#elif defined(__APPLE__) +#include +#include +#endif #include +#ifdef __linux__ +#include +#elif defined(__APPLE__) +#include +#include +#endif #endif #include "prx/libc/include/General.hpp" #include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/AppArchive.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/GuestArena.hpp" namespace { // Guest prefixes (without leading slashes) mapped to host directories, e.g. save-data mount points: @@ -51,6 +68,41 @@ PathAliases& Aliases() { return *aliases; } +std::size_t HostPageSize() { + static const std::size_t size = [] { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + return static_cast(info.dwPageSize); +#else + const long value = sysconf(_SC_PAGESIZE); + if (value <= 0) throw std::runtime_error("cannot query the host page size"); + return static_cast(value); +#endif + }(); + return size; +} + +bool CopyGuestBytes(void* destination, const void* source, std::size_t bytes) { + GuestArena::GuestArenaPrepareHostRead_nid_no_patch(source, bytes); +#ifdef _WIN32 + SIZE_T copied = 0; + return ReadProcessMemory(GetCurrentProcess(), source, destination, bytes, &copied) != 0 && copied == bytes; +#elif defined(__linux__) + iovec local{destination, bytes}; + iovec remote{const_cast(source), bytes}; + const auto copied = process_vm_readv(getpid(), &local, 1, &remote, 1, 0); + if (copied < 0 && errno != EFAULT) throw std::runtime_error("guest path: process_vm_readv failed with errno " + std::to_string(errno)); + return copied >= 0 && static_cast(copied) == bytes; +#elif defined(__APPLE__) + mach_vm_size_t copied = 0; + return mach_vm_read_overwrite(mach_task_self(), reinterpret_cast(source), bytes, + reinterpret_cast(destination), &copied) == KERN_SUCCESS && copied == bytes; +#else + throw std::runtime_error("checked guest path copies are not implemented on this host"); +#endif +} + std::string TrimSlashes(const char* path) { std::string s(path); std::size_t start = 0; @@ -489,6 +541,39 @@ int SetWorkingDirectory(WorkingDirectory& state, const std::filesystem::path& re } } +extern "C" int CopyGuestString_nid_no_patch(const char* source, std::size_t maximum, std::string& destination) { + if (source == nullptr) return 14; + const auto address = reinterpret_cast(source); + std::vector bytes; + try { + bytes.resize(maximum); + } catch (const std::bad_alloc&) { + return 12; + } + std::size_t offset = 0; + while (offset < bytes.size()) { + if (address > std::numeric_limits::max() - offset) return 14; + const auto current = address + offset; + const auto pageRemaining = HostPageSize() - current % HostPageSize(); + const auto count = std::min(pageRemaining, bytes.size() - offset); + if (!CopyGuestBytes(bytes.data() + offset, reinterpret_cast(current), count)) return 14; + if (const auto* end = static_cast(std::memchr(bytes.data() + offset, 0, count))) { + try { + destination.assign(bytes.data(), static_cast(end - bytes.data())); + } catch (const std::bad_alloc&) { + return 12; + } + return 0; + } + offset += count; + } + return 63; +} + +extern "C" int CopyGuestPath_nid_no_patch(const char* source, std::string& destination) { + return CopyGuestString_nid_no_patch(source, 1024, destination); +} + #ifndef _WIN32 namespace { std::mutex g_hostStandardErrorMutex; @@ -727,6 +812,39 @@ extern "C" std::filesystem::path ResolveArchivedPath_nid_no_patch(const char* pa return ResolveLocked(path, module, true); } +extern "C" bool GuestCopyOut_nid_no_patch(void* destination, const void* source, std::size_t bytes) { + if (bytes == 0) return true; + const auto start = reinterpret_cast(destination); + if (start == 0 || bytes > UINTPTR_MAX - start) return false; + const GuestArena::HostWrite guard(destination, bytes); + if (!guard.Open()) return false; +#ifdef _WIN32 + for (auto address = start; address < start + bytes;) { + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast(address), &memory, sizeof(memory)) != sizeof(memory) || memory.State != MEM_COMMIT || + (memory.Protect & PAGE_GUARD) != 0 || + (memory.Protect & (PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY)) == 0) return false; + address = reinterpret_cast(memory.BaseAddress) + memory.RegionSize; + } + std::memcpy(destination, source, bytes); + return true; +#elif defined(__linux__) + iovec local{const_cast(source), bytes}; + iovec remote{destination, bytes}; + const ssize_t copied = ::process_vm_writev(::getpid(), &local, 1, &remote, 1, 0); + if (copied < 0) { + if (errno == EFAULT) return false; + throw std::runtime_error("GuestCopyOut: process_vm_writev failed with errno " + std::to_string(errno)); + } + return static_cast(copied) == bytes; +#elif defined(__APPLE__) + return ::mach_vm_write(::mach_task_self(), reinterpret_cast(destination), + reinterpret_cast(source), static_cast(bytes)) == KERN_SUCCESS; +#else + throw std::runtime_error("GuestCopyOut: no checked write is available on this host"); +#endif +} + extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path) { auto resolved = ResolveLocked(path, false, true); AppArchiveRefuseOnly_nid_no_patch(resolved, path, "reading"); @@ -743,9 +861,15 @@ extern "C" std::filesystem::path RelinkedModulePath_nid_no_patch(const std::file const auto relinked = RelinkedModuleName(path); std::error_code error; if (std::filesystem::is_regular_file(relinked, error)) return relinked; + const auto parent = relinked.parent_path().empty() ? std::filesystem::path(".") : relinked.parent_path(); NameMatch names[] = {{relinked.filename().string()}, {path.filename().string(), std::nullopt, false, true}}; - MatchNames(relinked.parent_path().empty() ? "." : relinked.parent_path(), names); + MatchNames(parent, names); if (names[0].ambiguous || names[0].path) return MatchedModuleOrPath(path, relinked, names[0]); + for (const char* directory : {"prx", "sce_module", "sce_modules"}) { + NameMatch nested[] = {{relinked.filename().string()}}; + MatchNames(parent / directory, nested); + if (nested[0].ambiguous || nested[0].path) return MatchedModuleOrPath(path, relinked, nested[0]); + } return names[1].path ? *names[1].path : path; } diff --git a/core/libs/prx/libc/src/HeapDiagnostics.cpp b/core/libs/prx/libc/src/HeapDiagnostics.cpp index 626f036e4f..15f420f9c7 100644 --- a/core/libs/prx/libc/src/HeapDiagnostics.cpp +++ b/core/libs/prx/libc/src/HeapDiagnostics.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/Api.hpp" +#include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/HeapDiagnostics.hpp" #include @@ -77,13 +78,15 @@ void APS5_VABI sceLibcInternalHeapErrorReportForGame_nid_postfix(void* heap, voi const auto* state = static_cast(heap); std::uint32_t number = 0; std::memcpy(&number, state + MspaceNumberOffset, sizeof(number)); - std::FILE* output = GuestStandardOutputFile_nid_no_patch(); - std::fprintf(output, "[SceLibc] A heap error is detected.\n[SceLibc] -- Mspace Name --\n[SceLibc] %s", state + MspaceNameOffset); - if (number != 0) std::fprintf(output, " (%2u)", number); - std::fputc('\n', output); - if (error == SegmentOutOfRange) std::fprintf(output, "[SceLibc] -- Error reason --\n[SceLibc] segment out of range.\n"); - std::fprintf(output, "[SceLibc] pid:<0x%lx> tid:<0x%lx>\n", CurrentProcessId(), CurrentThreadId()); - std::fflush(output); + if (_Stdout_nid_postfix.IsOpen()) { + std::FILE* output = GuestStandardOutputFile_nid_no_patch(); + std::fprintf(output, "[SceLibc] A heap error is detected.\n[SceLibc] -- Mspace Name --\n[SceLibc] %s", state + MspaceNameOffset); + if (number != 0) std::fprintf(output, " (%2u)", number); + std::fputc('\n', output); + if (error == SegmentOutOfRange) std::fprintf(output, "[SceLibc] -- Error reason --\n[SceLibc] segment out of range.\n"); + std::fprintf(output, "[SceLibc] pid:<0x%lx> tid:<0x%lx>\n", CurrentProcessId(), CurrentThreadId()); + std::fflush(output); + } std::fprintf(stderr, "[libc] heap error 0x%x: heap %p, block %p, reported from guest 0x%llx\n", error, heap, block, static_cast(reinterpret_cast(__builtin_return_address(0)))); } diff --git a/core/libs/prx/libc/src/Hostname.cpp b/core/libs/prx/libc/src/Hostname.cpp index a8a74cc4af..b2e7999984 100644 --- a/core/libs/prx/libc/src/Hostname.cpp +++ b/core/libs/prx/libc/src/Hostname.cpp @@ -6,7 +6,7 @@ #include extern "C" int APS5_VABI gethostname_nid_postfix(char* name, std::size_t length) { - constexpr char hostname[] = "AnyPS5"; + constexpr char hostname[] = ""; if (name == nullptr) return 0; const int savedError = errno; const auto count = std::min(length, sizeof(hostname)); diff --git a/core/libs/prx/libc/src/Interfaces.cpp b/core/libs/prx/libc/src/Interfaces.cpp index 9b82031aae..f9ec682ec7 100644 --- a/core/libs/prx/libc/src/Interfaces.cpp +++ b/core/libs/prx/libc/src/Interfaces.cpp @@ -80,6 +80,22 @@ std::vector GuestAddress(const sockaddr* address) { return GuestIpv6(reinterpret_cast(&v6->sin6_addr), v6->sin6_scope_id); } +#ifndef _WIN32 +std::vector GuestNetmask(const sockaddr* netmask, int family) { + sockaddr_storage mask{}; + const std::size_t size = family == AF_INET ? sizeof(sockaddr_in) : sizeof(sockaddr_in6); +#ifdef __APPLE__ + const std::size_t length = std::min(netmask->sa_len, size); +#else + const std::size_t length = size; +#endif + if (length > offsetof(sockaddr, sa_data)) + std::memcpy(reinterpret_cast(&mask) + offsetof(sockaddr, sa_data), netmask->sa_data, length - offsetof(sockaddr, sa_data)); + mask.ss_family = static_cast(family); + return GuestAddress(reinterpret_cast(&mask)); +} +#endif + std::string CheckedName(const char* name) { const std::string checked(name); if (checked.empty() || checked.size() >= guestNameCapacity) @@ -302,7 +318,7 @@ int HostEntries(std::vector& entries) { #endif if (item->ifa_addr->sa_family != AF_INET && item->ifa_addr->sa_family != AF_INET6) continue; if (!item->ifa_netmask) throw std::runtime_error(std::string("getifaddrs: host address without a netmask on ") + item->ifa_name); - Entry entry{CheckedName(item->ifa_name), InterfaceFlags(item->ifa_flags), GuestAddress(item->ifa_addr), GuestAddress(item->ifa_netmask), {}}; + Entry entry{CheckedName(item->ifa_name), InterfaceFlags(item->ifa_flags), GuestAddress(item->ifa_addr), GuestNetmask(item->ifa_netmask, item->ifa_addr->sa_family), {}}; const int family = item->ifa_addr->sa_family; if (item->ifa_flags & IFF_POINTOPOINT) { if (item->ifa_dstaddr && item->ifa_dstaddr->sa_family == family) entry.destination = GuestAddress(item->ifa_dstaddr); diff --git a/core/libs/prx/libc/src/JumpBuffer.cpp b/core/libs/prx/libc/src/JumpBuffer.cpp index 77dca484e4..17c34cf9d0 100644 --- a/core/libs/prx/libc/src/JumpBuffer.cpp +++ b/core/libs/prx/libc/src/JumpBuffer.cpp @@ -1,12 +1,62 @@ +#include "prx/libc/include/JumpBuffer.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/general/AsmMacros.hpp" +#include +#include +#include +#include + +namespace JumpBuffer { +namespace { + +constexpr std::size_t MaskOffset = 72; +constexpr std::size_t MaskSavedOffset = 88; +constexpr int SigBlock = 1; +constexpr int SigSetmask = 3; + +std::atomic guestSigprocmask{nullptr}; + +} + +extern "C" { + +void GuestSigprocmaskRegister_nid_no_patch(Sigprocmask sigprocmask) { + guestSigprocmask.store(sigprocmask); +} + +void APS5_VABI JumpBufferSaveMask_nid_no_patch(unsigned char* buffer) { + const auto sigprocmask = guestSigprocmask.load(); + const std::int32_t saved = sigprocmask != nullptr && sigprocmask(SigBlock, nullptr, buffer + MaskOffset) == 0; + std::memcpy(buffer + MaskSavedOffset, &saved, sizeof(saved)); +} + +void APS5_VABI JumpBufferRestoreMask_nid_no_patch(const unsigned char* buffer) { + std::int32_t saved = 0; + std::memcpy(&saved, buffer + MaskSavedOffset, sizeof(saved)); + const auto sigprocmask = guestSigprocmask.load(); + if (saved != 0 && sigprocmask != nullptr) sigprocmask(SigSetmask, buffer + MaskOffset, nullptr); +} + +} + +} // setjmp/longjmp must capture the guest's own frame, so they are written directly in assembly with // the guest (System V) calling convention. The saved state fits the guest's 96-byte jmp_buf: -// return address, rbx, rsp, rbp, r12-r15, MXCSR and the x87 control word. +// return address, rbx, rsp, rbp, r12-r15, MXCSR, the x87 control word and, as FreeBSD's do, the +// signal mask at 72 with the flag sigsetjmp keeps at 88, once libkernel has registered sigprocmask. + +#if defined(_WIN32) || defined(__APPLE__) +#define APS5_JUMP_ASM_CALL(name) " call " APS5_ASM_SYMBOL(name) "\n" +#else +#define APS5_JUMP_ASM_CALL(name) " call " name "@PLT\n" +#endif asm(".text\n" APS5_ASM_FUNCTION("setjmp_nid_postfix") + " push %rdi\n" + APS5_JUMP_ASM_CALL("JumpBufferSaveMask_nid_no_patch") + " pop %rdi\n" " mov (%rsp), %rax\n" " mov %rax, 0(%rdi)\n" " mov %rbx, 8(%rdi)\n" @@ -22,15 +72,16 @@ asm(".text\n" " xor %eax, %eax\n" " ret\n" APS5_ASM_FUNCTION("longjmp_nid_postfix") -#ifdef _WIN32 " mov %rdi, %rbx\n" " mov %esi, %r12d\n" - " mov 16(%rdi), %rdi\n" " and $-16, %rsp\n" + APS5_JUMP_ASM_CALL("JumpBufferRestoreMask_nid_no_patch") +#ifdef _WIN32 + " mov 16(%rbx), %rdi\n" " call GuestStackSwitch_nid_no_patch\n" +#endif " mov %rbx, %rdi\n" " mov %r12d, %esi\n" -#endif " mov %esi, %eax\n" " test %eax, %eax\n" " jnz 1f\n" diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index 2546de6389..075be23d4b 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -301,6 +301,7 @@ constexpr std::array MakeClassificationTable() { if (value < 32 || value == 127) mask |= 0x80; if (value == ' ') mask |= 0x04; if (value >= '\t' && value <= '\r') mask |= 0x40; + if (value == '\t') mask |= 0x400; if (value >= 'A' && value <= 'Z') mask |= 0x02; if (value >= 'a' && value <= 'z') mask |= 0x10; if (value >= '0' && value <= '9') mask |= 0x20; @@ -328,18 +329,18 @@ constexpr auto g_lowerTable = MakeCaseTable(false); constexpr auto g_upperTable = MakeCaseTable(true); void ValidateCharacter(int value) { - if (value != EOF && (value < 0 || value > UCHAR_MAX)) throw std::invalid_argument("Invalid character value"); + if (value < SCHAR_MIN || value > UCHAR_MAX) throw std::invalid_argument("Invalid character value"); } -int ClassifyCharacter(int value, std::ctype_base::mask mask) { +int ClassifyCharacter(int value, short mask) { ValidateCharacter(value); if (value == EOF) return 0; - return std::use_facet>(std::locale::classic()).is(mask, static_cast(value)); + return g_classificationTable[static_cast(value < 0 ? value + UCHAR_MAX + 1 : value) + 1] & mask; } int ConvertCharacter(int value, bool upper) { ValidateCharacter(value); - if (value == EOF) return EOF; + if (value < 0) return value; const auto& facet = std::use_facet>(std::locale::classic()); const auto character = static_cast(value); return static_cast(upper ? facet.toupper(character) : facet.tolower(character)); @@ -349,18 +350,18 @@ int ConvertCharacter(int value, bool upper) { extern "C" { -int APS5_VABI isupper_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::upper); } -int APS5_VABI islower_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::lower); } -int APS5_VABI isalpha_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::alpha); } -int APS5_VABI isdigit_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::digit); } -int APS5_VABI isalnum_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::alnum); } -int APS5_VABI isspace_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::space); } -int APS5_VABI isblank_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::blank); } -int APS5_VABI iscntrl_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::cntrl); } -int APS5_VABI isprint_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::print); } -int APS5_VABI isgraph_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::graph); } -int APS5_VABI ispunct_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::punct); } -int APS5_VABI isxdigit_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::xdigit); } +int APS5_VABI isupper_nid_postfix(int c) { return ClassifyCharacter(c, 0x02); } +int APS5_VABI islower_nid_postfix(int c) { return ClassifyCharacter(c, 0x10); } +int APS5_VABI isalpha_nid_postfix(int c) { return ClassifyCharacter(c, 0x12); } +int APS5_VABI isdigit_nid_postfix(int c) { return ClassifyCharacter(c, 0x20); } +int APS5_VABI isalnum_nid_postfix(int c) { return ClassifyCharacter(c, 0x32); } +int APS5_VABI isspace_nid_postfix(int c) { return ClassifyCharacter(c, 0x44); } +int APS5_VABI isblank_nid_postfix(int c) { return ClassifyCharacter(c, 0x404); } +int APS5_VABI iscntrl_nid_postfix(int c) { return ClassifyCharacter(c, 0x80); } +int APS5_VABI isprint_nid_postfix(int c) { return ClassifyCharacter(c, 0x3e); } +int APS5_VABI isgraph_nid_postfix(int c) { return ClassifyCharacter(c, 0x3a); } +int APS5_VABI ispunct_nid_postfix(int c) { return ClassifyCharacter(c, 0x08); } +int APS5_VABI isxdigit_nid_postfix(int c) { return ClassifyCharacter(c, 0x01); } int APS5_VABI isascii_nid_postfix(int c) { return (c & ~0x7F) == 0; } int APS5_VABI toascii_nid_postfix(int c) { return c & 0x7F; } int APS5_VABI toupper_nid_postfix(int c) { return ConvertCharacter(c, true); } @@ -383,6 +384,9 @@ std::uint64_t _ZNSt7num_putIwSt19ostreambuf_iteratorIwSt11char_traitsIwEEE2idE_n std::uintptr_t _ZTVSt7num_putIwSt19ostreambuf_iteratorIwSt11char_traitsIwEEE_nid_postfix[14] {}; std::uint64_t _ZNSt8numpunctIcE2idE_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] {}; +std::uintptr_t _ZTVSt7num_getIwSt19istreambuf_iteratorIwSt11char_traitsIwEEE_nid_postfix[17] {}; std::streamoff _ZSt7_BADOFF_nid_postfix = -1; std::fpos_t _ZSt4_Fpz_nid_postfix {}; diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index d4732e12f3..91e8efa251 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -69,6 +69,13 @@ float SinhfExpm1(float x) { return (oneMinusTwoPowMinusK - (e - x)) * twoPowK; } +volatile float AcosfPio2Low = 7.5497894159e-08f; + +float AcosfRational(float z) { + constexpr float P0 = 1.6666586697e-01f, P1 = -4.2743422091e-02f, P2 = -8.6563630030e-03f, Q1 = -7.0662963390e-01f; + return z * (P0 + z * (P1 + z * P2)) / (1.0f + z * Q1); +} + float SinhfExp(float x) { constexpr float Ln2High = 6.9314575195e-01f, Ln2Low = 1.4286067653e-06f, InverseLn2 = 1.4426950216e+00f; constexpr float P1 = 1.6666625440e-1f, P2 = -2.7667332906e-3f; @@ -310,10 +317,23 @@ float APS5_VABI acosf_nid_postfix(float x) { if (std::fabs(x) > 1.0f) { return std::bit_cast(0xffc00000u); } - const int saved_errno = errno; - const float result = std::acos(x); - errno = saved_errno; - return result; + constexpr float Pi = 3.1415925026e+00f, Pio2High = 1.5707962513e+00f; + const std::uint32_t magnitude = std::bit_cast(x) & 0x7fffffffu; + if (magnitude == 0x3f800000u) return std::signbit(x) ? Pi + 2.0f * AcosfPio2Low : 0.0f; + if (magnitude < 0x3f000000u) { + if (magnitude <= 0x32800000u) return Pio2High + AcosfPio2Low; + return Pio2High - (x - (AcosfPio2Low - x * AcosfRational(x * x))); + } + if (std::signbit(x)) { + const float z = (1.0f + x) * 0.5f; + const float s = std::sqrt(z); + return Pi - 2.0f * (s + (AcosfRational(z) * s - AcosfPio2Low)); + } + const float z = (1.0f - x) * 0.5f; + const float s = std::sqrt(z); + const float high = std::bit_cast(std::bit_cast(s) & 0xfffff000u); + const float correction = (z - high * high) / (s + high); + return 2.0f * (high + (AcosfRational(z) * s + correction)); } float APS5_VABI atan2f_nid_postfix(float y, float x) { return std::atan2(y, x); } float APS5_VABI hypotf_nid_postfix(float x, float y) { return static_cast(std::hypot(static_cast(x), static_cast(y))); } @@ -329,6 +349,49 @@ double APS5_VABI hypot_nid_postfix(double x, double y) { } float APS5_VABI log10f_nid_postfix(float x) { return std::log10(x); } float APS5_VABI logbf_nid_postfix(float x) { return std::logb(x); } +float APS5_VABI exp2f_nid_postfix(float x) { + static constexpr double exp2ft[16] = { + 0x1.6a09e667f3bcdp-1, 0x1.7a11473eb0187p-1, 0x1.8ace5422aa0dbp-1, 0x1.9c49182a3f090p-1, + 0x1.ae89f995ad3adp-1, 0x1.c199bdd85529cp-1, 0x1.d5818dcfba487p-1, 0x1.ea4afa2a490dap-1, + 0x1.0000000000000p+0, 0x1.0b5586cf9890fp+0, 0x1.172b83c7d517bp+0, 0x1.2387a6e756238p+0, + 0x1.306fe0a31b715p+0, 0x1.3dea64c123422p+0, 0x1.4bfdad5362a27p+0, 0x1.5ab07dd485429p+0, + }; + static volatile float huge = 0x1p100f; + static volatile float twom100 = 0x1p-100f; + const float redux = 0x1.8p23f / 16; + const float P1 = 0x1.62e430p-1f; + const float P2 = 0x1.ebfbe0p-3f; + const float P3 = 0x1.c6b348p-5f; + const float P4 = 0x1.3b2c9cp-7f; + const std::uint32_t hx = std::bit_cast(x); + const std::uint32_t ix = hx & 0x7fffffff; + if (ix >= 0x43000000) { + if (ix >= 0x7f800000) { + return (ix & 0x7fffff) != 0 || (hx & 0x80000000) == 0 ? x + x : 0.0f; + } + if (x >= 0x1.0p7f) { + errno = ERANGE; + return huge * huge; + } + if (x <= -0x1.2cp7f) { + errno = ERANGE; + return twom100 * twom100; + } + } else if (ix <= 0x33000000) { + return 1.0f + x; + } + volatile float rounded = x + redux; + float t = rounded; + std::uint32_t i0 = std::bit_cast(t) + 8; + const std::int32_t k = static_cast(i0 >> 4) << 20; + i0 &= 15; + t -= redux; + const double z = x - t; + const double twopk = std::bit_cast(static_cast(static_cast(0x3ff00000 + k)) << 32); + const double u = exp2ft[i0] * z; + const double tv = exp2ft[i0] + u * (P1 + z * P2) + u * (z * z) * (P3 + z * P4); + return static_cast(tv * twopk); +} double APS5_VABI exp2_nid_postfix(double x) { int saved_errno = errno; double result = std::exp2(x); @@ -353,9 +416,9 @@ int APS5_VABI __isfinitef_nid_postfix(float x) { return std::isfinite(x) ? 1 : 0 int APS5_VABI __isnormal_nid_postfix(double x) { return std::isnormal(x) ? 1 : 0; } int APS5_VABI __isnormalf_nid_postfix(float x) { return std::isnormal(x) ? 1 : 0; } int APS5_VABI __isinff_nid_postfix(float x) { return std::isinf(x) ? 1 : 0; } -float APS5_VABI nearbyintf_nid_postfix(float x) { return std::nearbyintf(x); } +float APS5_VABI nearbyintf_nid_postfix(float x) { return std::isnan(x) ? x + x : std::nearbyintf(x); } long double APS5_VABI nearbyintl_nid_postfix(long double x) { return std::nearbyint(x); } -double APS5_VABI nearbyint_nid_postfix(double x) { return std::nearbyint(x); } +double APS5_VABI nearbyint_nid_postfix(double x) { return std::isnan(x) ? x + x : std::nearbyint(x); } double APS5_VABI cbrt_nid_postfix(double x) { return std::cbrt(x); } double APS5_VABI asin_nid_postfix(double x) { @@ -378,12 +441,12 @@ float APS5_VABI copysignf_nid_postfix(float x, float y) { return std::copysign(x, y); } int APS5_VABI abs_nid_postfix(int x) { return x < 0 ? static_cast(0u - static_cast(x)) : x; } -double APS5_VABI trunc_nid_postfix(double x) { return std::trunc(x); } -double APS5_VABI ceil_nid_postfix(double x) { return std::ceil(x); } -double APS5_VABI floor_nid_postfix(double x) { return std::floor(x); } -float APS5_VABI truncf_nid_postfix(float x) { return std::trunc(x); } -float APS5_VABI ceilf_nid_postfix(float x) { return std::ceil(x); } -float APS5_VABI floorf_nid_postfix(float x) { return std::floor(x); } +double APS5_VABI trunc_nid_postfix(double x) { return std::isnan(x) ? x + x : std::trunc(x); } +double APS5_VABI ceil_nid_postfix(double x) { return std::isnan(x) ? x + x : std::ceil(x); } +double APS5_VABI floor_nid_postfix(double x) { return std::isnan(x) ? x + x : std::floor(x); } +float APS5_VABI truncf_nid_postfix(float x) { return std::isnan(x) ? x + x : std::trunc(x); } +float APS5_VABI ceilf_nid_postfix(float x) { return std::isnan(x) ? x + x : std::ceil(x); } +float APS5_VABI floorf_nid_postfix(float x) { return std::isnan(x) ? x + x : std::floor(x); } double APS5_VABI fma_nid_postfix(double x, double y, double z) { return std::fma(x, y, z); } float APS5_VABI fmaf_nid_postfix(float x, float y, float z) { return std::fma(x, y, z); } double APS5_VABI fmin_nid_postfix(double x, double y) { return GuestFmin(x, y); } @@ -461,7 +524,95 @@ double APS5_VABI atan_nid_postfix(double x) { return std::atan(x); } double APS5_VABI tan_nid_postfix(double x) { return std::tan(x); } double APS5_VABI log2_nid_postfix(double x) { return std::log2(x); } double APS5_VABI log_nid_postfix(double x) { return std::log(x); } -double APS5_VABI log1p_nid_postfix(double x) { return std::log1p(x); } +double APS5_VABI log1p_nid_postfix(double x) { + const double ln2_hi = 6.93147180369123816490e-01; + const double ln2_lo = 1.90821492927058770002e-10; + const double two54 = 1.80143985094819840000e+16; + const double Lp1 = 6.666666666666735130e-01; + const double Lp2 = 3.999999999940941908e-01; + const double Lp3 = 2.857142874366239149e-01; + const double Lp4 = 2.222219843214978396e-01; + const double Lp5 = 1.818357216161805012e-01; + const double Lp6 = 1.531383769920937332e-01; + const double Lp7 = 1.479819860511658591e-01; + static volatile double vzero = 0.0; + const std::int32_t hx = static_cast(std::bit_cast(x) >> 32); + const std::int32_t ax = hx & 0x7fffffff; + std::int32_t k = 1; + std::int32_t hu = 0; + double f = 0.0; + double c = 0.0; + if (hx < 0x3fda827a) { + if (ax >= 0x3ff00000) { + if (x == -1.0) { + return -two54 / vzero; + } + errno = EDOM; + return (x - x) / (x - x); + } + if (ax < 0x3e200000) { + if (two54 + x > 0.0 && ax < 0x3c900000) { + return x; + } + return x - x * x * 0.5; + } + if (hx > 0 || hx <= static_cast(0xbfd2bec4)) { + k = 0; + f = x; + hu = 1; + } + } + if (hx >= 0x7ff00000) { + return x + x; + } + if (k != 0) { + double u; + if (hx < 0x43400000) { + u = 1.0 + x; + hu = static_cast(std::bit_cast(u) >> 32); + k = (hu >> 20) - 1023; + c = k > 0 ? 1.0 - (u - x) : x - (u - 1.0); + c /= u; + } else { + u = x; + hu = static_cast(std::bit_cast(u) >> 32); + k = (hu >> 20) - 1023; + c = 0; + } + hu &= 0x000fffff; + const std::uint64_t ulow = std::bit_cast(u) & 0xffffffffull; + if (hu < 0x6a09e) { + u = std::bit_cast((static_cast(static_cast(hu | 0x3ff00000)) << 32) | ulow); + } else { + k += 1; + u = std::bit_cast((static_cast(static_cast(hu | 0x3fe00000)) << 32) | ulow); + hu = (0x00100000 - hu) >> 2; + } + f = u - 1.0; + } + const double hfsq = 0.5 * f * f; + if (hu == 0) { + if (f == 0.0) { + if (k == 0) { + return 0.0; + } + c += k * ln2_lo; + return k * ln2_hi + c; + } + const double R = hfsq * (1.0 - 0.66666666666666666 * f); + if (k == 0) { + return f - R; + } + return k * ln2_hi - ((R - (k * ln2_lo + c)) - f); + } + const double s = f / (2.0 + f); + const double z = s * s; + const double R = z * (Lp1 + z * (Lp2 + z * (Lp3 + z * (Lp4 + z * (Lp5 + z * (Lp6 + z * Lp7)))))); + if (k == 0) { + return f - (hfsq - s * (hfsq + R)); + } + return k * ln2_hi - ((hfsq - (s * (hfsq + R) + (k * ln2_lo + c))) - f); +} float APS5_VABI log1pf_nid_postfix(float x) { return std::log1p(x); } static double Expm1(double x) { const double tiny = 1.0e-300; @@ -911,13 +1062,17 @@ float APS5_VABI acoshf_nid_postfix(float x) { constexpr float ln2 = 6.9314718246e-01f; const std::uint32_t bits = std::bit_cast(x); const std::uint32_t magnitude = bits & 0x7fffffffu; - if ((bits & 0x80000000u) != 0 || magnitude < 0x3f800000u) return (x - x) / (x - x); + if ((bits & 0x80000000u) != 0 || magnitude < 0x3f800000u) { + _Feraise_nid_postfix(0x01); + errno = EDOM; + return (x - x) / (x - x); + } if (magnitude >= 0x7f800000u) return x + x; if (magnitude == 0x3f800000u) return 0.0f; - if (magnitude >= 0x4d800000u) return std::log(x) + ln2; + if (magnitude >= 0x4d800000u) return logf_nid_postfix(x) + ln2; if (magnitude > 0x40000000u) { const float square = x * x; - return std::log(2.0f * x - 1.0f / (x + std::sqrt(square - 1.0f))); + return logf_nid_postfix(2.0f * x + (-1.0f / (x + std::sqrt(square - 1.0f)))); } const float t = x - 1.0f; return AtanhfLog1p(t + std::sqrt(2.0f * t + t * t)); @@ -1023,7 +1178,6 @@ double APS5_VABI __powidf2_nid_postfix(double base, int exponent) { return reciprocal ? 1.0 / result : result; } float APS5_VABI expf_nid_postfix(float x) { return std::exp(x); } -float APS5_VABI exp2f_nid_postfix(float x) { return std::exp2(x); } float APS5_VABI logf_nid_postfix(float x) { const float ln2_hi = static_cast(6.9313812256e-01); const float ln2_lo = static_cast(9.0580006145e-06); @@ -1119,8 +1273,65 @@ float APS5_VABI cbrtf_nid_postfix(float x) { return static_cast(T); } float APS5_VABI remainderf_nid_postfix(float x, float y) { return std::remainder(x, y); } -float APS5_VABI nextafterf_nid_postfix(float x, float y) { return std::nextafter(x, y); } -double APS5_VABI nextafter_nid_postfix(double x, double y) { return std::nextafter(x, y); } + +float APS5_VABI nextafterf_nid_postfix(float x, float y) { + const std::uint32_t ux = std::bit_cast(x); + const std::uint32_t uy = std::bit_cast(y); + if ((ux & 0x7fffffffu) > 0x7f800000u || (uy & 0x7fffffffu) > 0x7f800000u) { + volatile float sum = x + y; + (void)sum; + return std::bit_cast(((ux & 0x7fffffffu) > 0x7f800000u ? ux : uy) | 0x00400000u); + } + if (x == y) { + return y; + } + if ((ux & 0x7fffffffu) == 0) { + const volatile float smallest = std::bit_cast((uy & 0x80000000u) | 1u); + volatile float product = smallest * smallest; + return product == smallest ? product : smallest; + } + const bool towardZero = ((ux ^ uy) & 0x80000000u) != 0 || ux > uy; + const std::uint32_t next = towardZero ? ux - 1 : ux + 1; + const std::uint32_t exponent = next & 0x7f800000u; + if (exponent == 0x7f800000u) { + _Feraise_nid_postfix(0x08); + return x + x; + } + if (exponent == 0) { + volatile float product = x * x; + if (product != x) _Feraise_nid_postfix(0x10); + } + return std::bit_cast(next); +} +double APS5_VABI nextafter_nid_postfix(double x, double y) { + const std::uint64_t ux = std::bit_cast(x); + const std::uint64_t uy = std::bit_cast(y); + if ((ux & 0x7fffffffffffffffull) > 0x7ff0000000000000ull || (uy & 0x7fffffffffffffffull) > 0x7ff0000000000000ull) { + volatile double sum = x + y; + (void)sum; + return std::bit_cast(((ux & 0x7fffffffffffffffull) > 0x7ff0000000000000ull ? ux : uy) | 0x0008000000000000ull); + } + if (x == y) { + return y; + } + if ((ux & 0x7fffffffffffffffull) == 0) { + const volatile double smallest = std::bit_cast((uy & 0x8000000000000000ull) | 1u); + volatile double product = smallest * smallest; + return product == smallest ? product : smallest; + } + const bool towardZero = ((ux ^ uy) & 0x8000000000000000ull) != 0 || ux > uy; + const std::uint64_t next = towardZero ? ux - 1 : ux + 1; + const std::uint64_t exponent = next & 0x7ff0000000000000ull; + if (exponent == 0x7ff0000000000000ull) { + _Feraise_nid_postfix(0x08); + return x + x; + } + if (exponent == 0) { + volatile double product = x * x; + if (product != x) _Feraise_nid_postfix(0x10); + } + return std::bit_cast(next); +} int APS5_VABI __isfinite_nid_postfix(double x) { return std::isfinite(x) ? 1 : 0; } int APS5_VABI __isnan_nid_postfix(double x) { return std::isnan(x) ? 1 : 0; } int APS5_VABI __isinf_nid_postfix(double x) { return std::isinf(x) ? 1 : 0; } @@ -1136,7 +1347,31 @@ double APS5_VABI nan_nid_postfix(const char* tagp) { double APS5_VABI modf_nid_postfix(double x, double* integral) { return std::modf(x, integral); } float APS5_VABI modff_nid_postfix(float x, float* integral) { return std::modf(x, integral); } -double APS5_VABI tanh_nid_postfix(double x) { return std::tanh(x); } +double APS5_VABI tanh_nid_postfix(double x) { + static volatile double tiny = 1.0e-300; + const double huge = 1.0e300; + const std::int32_t jx = static_cast(std::bit_cast(x) >> 32); + const std::int32_t ix = jx & 0x7fffffff; + if (ix >= 0x7ff00000) { + return jx >= 0 ? 1.0 / x + 1.0 : 1.0 / x - 1.0; + } + double z; + if (ix < 0x40360000) { + if (ix < 0x3e300000 && huge + x > 1.0) { + return x; + } + if (ix >= 0x3ff00000) { + const double t = Expm1(2.0 * std::fabs(x)); + z = 1.0 - 2.0 / (t + 2.0); + } else { + const double t = Expm1(-2.0 * std::fabs(x)); + z = -t / (t + 2.0); + } + } else { + z = 1.0 - tiny; + } + return jx >= 0 ? z : -z; +} float APS5_VABI tanhf_nid_postfix(float x) { return std::tanh(x); } long double APS5_VABI atanl_nid_postfix(long double x) { return std::atan(x); } long double APS5_VABI sinl_nid_postfix(long double x) { return std::sin(x); } @@ -1303,6 +1538,50 @@ double APS5_VABI _Cosh_nid_postfix(double x, double y) { return std::scalbn(scale * (0.5 * std::exp(reduced)), scaleExponent + exponent); } +long double APS5_VABI _LCosh_nid_postfix(long double x, long double y) { + if (!std::isfinite(x) || !std::isfinite(y)) return y * std::cosh(x); + if (y == 0.0L) return std::copysign(0.0L, y); + constexpr long double Ln2High = 0x1.62e42fefa0000p-1L; + constexpr long double Ln2Low = 0x1.cf79abc9e3b398p-40L; + constexpr long double InverseLn2 = 0x1.71547652b82fe178p+0L; + const long double magnitude = std::abs(x); + if (magnitude <= (std::numeric_limits::max_exponent - 1) * (Ln2High + Ln2Low)) + return y * std::cosh(x); + int scaleExponent; + const long double scale = std::frexp(y, &scaleExponent); + constexpr int MaximumExponentSpan = std::numeric_limits::max_exponent - + std::numeric_limits::min_exponent + std::numeric_limits::digits + 4; + if (magnitude > MaximumExponentSpan * (Ln2High + Ln2Low)) + return std::scalbn(scale, INT_MAX); + const int exponent = static_cast(magnitude * InverseLn2); + const long double reduced = std::fma(-exponent, Ln2Low, std::fma(-exponent, Ln2High, magnitude)); + return std::scalbn(scale * (0.5L * std::exp(reduced)), scaleExponent + exponent); +} + +long double APS5_VABI _LSinh_nid_postfix(long double x, long double y) { + if (!std::isfinite(x) || !std::isfinite(y)) return y * std::sinh(x); + if (y == 0.0L) { + const bool negative = std::signbit(x) != std::signbit(y); + return std::copysign(0.0L, negative ? -1.0L : 1.0L); + } + constexpr long double Ln2High = 0x1.62e42fefa0000p-1L; + constexpr long double Ln2Low = 0x1.cf79abc9e3b398p-40L; + constexpr long double InverseLn2 = 0x1.71547652b82fe178p+0L; + const long double magnitude = std::abs(x); + if (magnitude <= (std::numeric_limits::max_exponent - 1) * (Ln2High + Ln2Low)) + return y * std::sinh(x); + int scaleExponent; + const long double scale = std::frexp(y, &scaleExponent); + constexpr int MaximumExponentSpan = std::numeric_limits::max_exponent - + std::numeric_limits::min_exponent + std::numeric_limits::digits + 4; + const long double sign = std::copysign(1.0L, x); + if (magnitude > MaximumExponentSpan * (Ln2High + Ln2Low)) + return sign * std::scalbn(scale, INT_MAX); + const int exponent = static_cast(magnitude * InverseLn2); + const long double reduced = std::fma(-exponent, Ln2Low, std::fma(-exponent, Ln2High, magnitude)); + return sign * std::scalbn(scale * (0.5L * std::exp(reduced)), scaleExponent + exponent); +} + int APS5_VABI _Fltrounds_nid_postfix(void) { return SonyFltrounds(); } diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index 17f1889ed0..8d7c88c0d9 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -281,6 +282,90 @@ int APS5_VABI daemon_nid_postfix(int noChdir, int noClose) { return -1; } +#ifdef _WIN32 +namespace { +struct ChildProcess { + std::int32_t processId; + std::int64_t processHandle; +}; + +std::mutex childProcessMutex; +std::vector childProcesses; + +int ChildProcessWaitStatus(DWORD code) { + switch (code) { + case 0xC0000005u: + case 0xC00000FDu: return 11; + case 0xC000001Du: + case 0xC0000096u: return 4; + case 0x80000003u: return 5; + case 0xC0000094u: + case 0xC0000095u: return 8; + case 0xC0000409u: + case 0xC0000374u: return 6; + default: return static_cast(code & 0xffu) << 8; + } +} +} + +extern "C" void RegisterChildProcess_nid_no_patch(std::int32_t processId, std::int64_t processHandle) { + std::lock_guard lock(childProcessMutex); + childProcesses.push_back({processId, processHandle}); +} + +extern "C" std::int64_t WaitChildProcess_nid_no_patch(std::int32_t processId, unsigned options, int* status) { + const bool noWait = (options & 1u) != 0; + const auto matches = [&](const ChildProcess& child) { return processId <= 0 || child.processId == processId; }; + { + std::lock_guard lock(childProcessMutex); + if (std::none_of(childProcesses.begin(), childProcesses.end(), matches)) return -2; + } + int encoded = 0; + while (true) { + std::int32_t reaped = 0; + { + std::lock_guard lock(childProcessMutex); + for (std::size_t index = 0; index < childProcesses.size(); ++index) { + auto& child = childProcesses[index]; + if (!matches(child)) continue; + const HANDLE handle = reinterpret_cast(child.processHandle); + if (WaitForSingleObject(handle, 0) != WAIT_OBJECT_0) continue; + DWORD code = 0; + if (!GetExitCodeProcess(handle, &code)) { + *__error_nid_postfix() = 14; + return -1; + } + encoded = ChildProcessWaitStatus(code); + CloseHandle(handle); + reaped = child.processId; + childProcesses.erase(childProcesses.begin() + static_cast(index)); + break; + } + } + if (reaped != 0) { + if (status != nullptr) *status = encoded; + return reaped; + } + std::vector handles; + { + const std::lock_guard lock(childProcessMutex); + for (const auto& child : childProcesses) { + if (!matches(child)) continue; + handles.push_back(reinterpret_cast(child.processHandle)); + } + } + if (handles.empty()) return -2; + if (noWait) return 0; + constexpr std::size_t groupSize = MAXIMUM_WAIT_OBJECTS; + bool timedOut = true; + for (std::size_t start = 0; start < handles.size() && timedOut; start += groupSize) { + const std::size_t count = std::min(handles.size() - start, groupSize); + timedOut = WaitForMultipleObjects(static_cast(count), handles.data() + start, FALSE, 250) == WAIT_TIMEOUT; + } + } +} +#endif + } namespace { diff --git a/core/libs/prx/libc/src/Random.cpp b/core/libs/prx/libc/src/Random.cpp index 03aba20b2e..67bd066466 100644 --- a/core/libs/prx/libc/src/Random.cpp +++ b/core/libs/prx/libc/src/Random.cpp @@ -121,7 +121,7 @@ std::uint64_t NextRand48(std::uint16_t* seed, const char* function) { extern "C" { double APS5_VABI erand48_nid_postfix(std::uint16_t* seed) { - return std::ldexp(static_cast(NextRand48(seed, __func__)), -48); + return std::ldexp(static_cast(NextRand48(seed, __func__) >> 16), -32); } std::int64_t APS5_VABI nrand48_nid_postfix(std::uint16_t* seed) { @@ -134,7 +134,7 @@ std::int64_t APS5_VABI jrand48_nid_postfix(std::uint16_t* seed) { double APS5_VABI drand48_nid_postfix() { std::lock_guard lock(g_randomLock); - return std::ldexp(static_cast(NextRand48(g_rand48Seed, __func__)), -48); + return std::ldexp(static_cast(NextRand48(g_rand48Seed, __func__) >> 16), -32); } std::int64_t APS5_VABI lrand48_nid_postfix() { diff --git a/core/libs/prx/libc/src/Rename.cpp b/core/libs/prx/libc/src/Rename.cpp index 36454860ae..cdad0eff61 100644 --- a/core/libs/prx/libc/src/Rename.cpp +++ b/core/libs/prx/libc/src/Rename.cpp @@ -42,6 +42,16 @@ std::error_code RenameNative_nid_no_patch(const std::filesystem::path& source, c return error; } +#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, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) return GetLastError() == ERROR_CANT_RESOLVE_FILENAME; + CloseHandle(handle); + return false; +} +#endif + static int RenamePath_nid_no_patch(const std::filesystem::path& source, const std::filesystem::path& destination) { constexpr int GuestEnoent = 2; constexpr int GuestEnotdir = 20; @@ -107,10 +117,17 @@ static int SourceLookupError_nid_no_patch(const std::filesystem::path& source) { } extern "C" int RenameGuestPath_nid_no_patch(const char* from, const char* to) { - if (!from) { errno = 14; return -1; } - if (!*from) { errno = 2; return -1; } + std::string fromCopy; + if (const int error = CopyGuestPath_nid_no_patch(from, fromCopy)) { errno = error; return -1; } + from = fromCopy.c_str(); + if (fromCopy.empty()) { errno = 2; return -1; } if (const int dot = RenameDotError_nid_no_patch(from)) { errno = dot; return -1; } - const int destinationError = !to ? 14 : !*to ? 2 : RenameDotError_nid_no_patch(to); + std::string toCopy; + int destinationError = CopyGuestPath_nid_no_patch(to, toCopy); + if (destinationError == 0) { + to = toCopy.c_str(); + destinationError = toCopy.empty() ? 2 : RenameDotError_nid_no_patch(to); + } try { const auto source = ResolvePath_nid_no_patch(from); if (destinationError) { @@ -124,6 +141,13 @@ extern "C" int RenameGuestPath_nid_no_patch(const char* from, const char* to) { std::error_code status; if (HasNonDirectoryPrefix_nid_no_patch(source) || (std::filesystem::exists(source, status) && HasNonDirectoryPrefix_nid_no_patch(destination))) error = 20; } +#ifdef _WIN32 + constexpr int GuestEloop = 62; + if (error == 2 || error == 20) { + if (BelowSymbolicLinkLoop(source.parent_path())) error = GuestEloop; + else if (SourceLookupError_nid_no_patch(source) == 0 && BelowSymbolicLinkLoop(destination.parent_path())) error = GuestEloop; + } +#endif if (error) { errno = error; return -1; } RecordWrittenPath_nid_no_patch(source); RecordWrittenPath_nid_no_patch(destination); diff --git a/core/libs/prx/libc/src/RuneLocale.cpp b/core/libs/prx/libc/src/RuneLocale.cpp index 18e5aceb1d..f038c5cd42 100644 --- a/core/libs/prx/libc/src/RuneLocale.cpp +++ b/core/libs/prx/libc/src/RuneLocale.cpp @@ -110,6 +110,7 @@ int APS5_VABI iswpunct_nid_postfix(std::uint32_t c) { return GuestRune::Is(c, Gu int APS5_VABI iswspace_nid_postfix(std::uint32_t c) { return GuestRune::Is(c, GuestRune::Space); } int APS5_VABI iswupper_nid_postfix(std::uint32_t c) { return GuestRune::Is(c, GuestRune::Upper); } int APS5_VABI iswxdigit_nid_postfix(std::uint32_t c) { return GuestRune::Is(c, GuestRune::Hex); } +int APS5_VABI iswascii_nid_postfix(std::uint32_t c) { return (c & ~0x7Fu) == 0; } int APS5_VABI iswctype_nid_postfix(std::uint32_t c, std::uint64_t mask) { return GuestRune::Is(c, mask); } int APS5_VABI _Iswctype_nid_postfix(std::uint32_t c, std::uint64_t type) { diff --git a/core/libs/prx/libc/src/ScanningWide.cpp b/core/libs/prx/libc/src/ScanningWide.cpp index 0390ecd6cb..cccfa535b1 100644 --- a/core/libs/prx/libc/src/ScanningWide.cpp +++ b/core/libs/prx/libc/src/ScanningWide.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/Api.hpp" +#include #include #include #include @@ -375,15 +376,18 @@ private: const std::string field = Field(directive.width, FloatFit); if (FloatFit(field) != Fit::Valid) return Outcome::Matching; if (target != nullptr) { + const int savedError = errno; if (directive.length == "L") { const long double value = std::strtold(field.c_str(), nullptr); std::memcpy(target, &value, sizeof(value)); } else if (directive.length == "l") { const double value = std::strtod(field.c_str(), nullptr); std::memcpy(target, &value, sizeof(value)); + errno = savedError; } else { const float value = std::strtof(field.c_str(), nullptr); std::memcpy(target, &value, sizeof(value)); + errno = savedError; } ++assigned; } diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index 958616a565..b40f822753 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -125,6 +125,7 @@ std::int32_t APS5_VABI fgetwc_nid_postfix(FileStream* stream) { } std::int32_t APS5_VABI ungetwc_nid_postfix(std::int32_t value, FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } if (value == -1) return -1; if (value < 0 || value > 255) { errno = 86; diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 224d291524..508da2aa04 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -21,6 +21,16 @@ int CompareUnits(char16_t left, char16_t right) { return left < right ? -1 : 1; } +int ByteDifference(const void* s1, const void* s2, size_t n, bool stopAtNul) { + const auto* left = static_cast(s1); + const auto* right = static_cast(s2); + for (size_t index = 0; index < n; ++index) { + if (left[index] != right[index]) return left[index] - right[index]; + if (stopAtNul && left[index] == 0) return 0; + } + return 0; +} + bool Contains(const char16_t* set, char16_t character) { for (; *set != 0; ++set) { if (*set == character) return true; @@ -246,7 +256,7 @@ void* APS5_VABI memmove_nid_postfix(void* dest, const void* src, size_t n) { } int APS5_VABI memcmp_nid_postfix(const void* s1, const void* s2, size_t n) { - return std::memcmp(s1, s2, n); + return std::memcmp(s1, s2, n) == 0 ? 0 : ByteDifference(s1, s2, n, false); } const void* APS5_VABI memchr_nid_postfix(const void* s, int c, size_t n) { @@ -290,11 +300,11 @@ void* APS5_VABI memmem_nid_postfix(const void* haystack, size_t haystacklen, con } int APS5_VABI strcmp_nid_postfix(const char* s1, const char* s2) { - return std::strcmp(s1, s2); + return std::strcmp(s1, s2) == 0 ? 0 : ByteDifference(s1, s2, std::numeric_limits::max(), true); } int APS5_VABI strncmp_nid_postfix(const char* s1, const char* s2, size_t n) { - return std::strncmp(s1, s2, n); + return std::strncmp(s1, s2, n) == 0 ? 0 : ByteDifference(s1, s2, n, true); } size_t APS5_VABI strlen_nid_postfix(const char* s) { @@ -477,7 +487,11 @@ size_t APS5_VABI strspn_nid_postfix(const char* s, const char* accept) { } int APS5_VABI strcoll_nid_postfix(const char* s1, const char* s2) { - return std::strcmp(s1, s2); + for (;; ++s1, ++s2) { + const int a = static_cast(*s1); + const int b = static_cast(*s2); + if (a != b || a == 0) return a - b; + } } int APS5_VABI strncpy_s_nid_postfix(char* dest, size_t destsz, const char* src, size_t count) { diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index 8f13974d7a..7b67acbcdb 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -48,11 +48,27 @@ timespec XtimeDeadline(const Xtime& time, const char* function) { return {static_cast(seconds), static_cast(nanos)}; } +std::chrono::system_clock::time_point XtimePoint(const Xtime& time, const char* function) { + const auto deadline = XtimeDeadline(time, function); + constexpr auto maxDuration = std::chrono::system_clock::duration::max(); + constexpr auto minDuration = std::chrono::system_clock::duration::min(); + constexpr auto maxSeconds = std::chrono::duration_cast(maxDuration).count(); + constexpr auto minSeconds = std::chrono::duration_cast(minDuration).count(); + constexpr auto maxNanos = std::chrono::duration_cast(maxDuration - std::chrono::seconds(maxSeconds)).count(); + if (deadline.tv_sec > maxSeconds || (deadline.tv_sec == maxSeconds && deadline.tv_nsec > maxNanos) || + deadline.tv_sec < minSeconds) + throw std::overflow_error(std::string(function) + ": xtime seconds overflow"); + return std::chrono::system_clock::time_point( + std::chrono::seconds(deadline.tv_sec) + + std::chrono::ceil(std::chrono::nanoseconds(deadline.tv_nsec))); +} + constexpr int threadSuccess = 0; constexpr int threadNomem = 1; constexpr int threadTimedout = 2; constexpr int threadBusy = 3; constexpr int threadError = 4; +constexpr int threadTimedlockError = 5; constexpr int mutexRecursive = 0x100; @@ -72,14 +88,61 @@ struct DinkumwareNativeMutex { void lock() { EnterCriticalSection(&value); } void unlock() { LeaveCriticalSection(&value); } bool try_lock() { return TryEnterCriticalSection(&value) != FALSE; } + + template + bool try_lock_until(const std::chrono::time_point& deadline) { + if (try_lock()) + return true; + for (;;) { + const auto now = Clock::now(); + if (now >= deadline) + return false; + const auto remaining = std::chrono::ceil(deadline - now).count(); + Sleep(static_cast(std::min(remaining, 1))); + if (try_lock()) { + if (Clock::now() <= deadline) + return true; + unlock(); + return false; + } + } + } }; struct DinkumwareNativeCondition { CONDITION_VARIABLE value = CONDITION_VARIABLE_INIT; + + void notify_one() { WakeConditionVariable(&value); } + void notify_all() { WakeAllConditionVariable(&value); } + + void wait(std::unique_lock& lock) { + if (SleepConditionVariableCS(&value, &lock.mutex()->value, INFINITE) == FALSE) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "_Cnd_wait: condition wait failed"); + } + + template + std::cv_status wait_until(std::unique_lock& lock, + const std::chrono::time_point& deadline) { + for (;;) { + const auto now = Clock::now(); + if (now >= deadline) + return std::cv_status::timeout; + const auto remaining = std::chrono::ceil(deadline - now).count(); + const auto largest = static_cast(std::numeric_limits::max() - 1U); + const auto milliseconds = static_cast(std::min(remaining, largest)); + if (SleepConditionVariableCS(&value, &lock.mutex()->value, milliseconds) != FALSE) + return std::cv_status::no_timeout; + const DWORD error = GetLastError(); + if (error != ERROR_TIMEOUT) + throw std::system_error(static_cast(error), std::system_category(), "_Cnd_timedwait: condition wait failed"); + if (Clock::now() >= deadline) + return std::cv_status::timeout; + } + } }; #else -using DinkumwareNativeMutex = std::mutex; -using DinkumwareNativeCondition = std::condition_variable; +using DinkumwareNativeMutex = std::timed_mutex; +using DinkumwareNativeCondition = std::condition_variable_any; #endif struct DinkumwareMutex { @@ -95,32 +158,6 @@ struct DinkumwareCondition { DinkumwareNativeCondition native; }; -#ifdef _WIN32 -DWORD WaitMillisecondsUntil(std::chrono::system_clock::time_point deadline) { - const auto now = std::chrono::system_clock::now(); - if (now >= deadline) - return 0; - const auto remaining = std::chrono::ceil(deadline - now).count(); - constexpr auto largest = static_cast(std::numeric_limits::max() - 1U); - return static_cast(std::min(remaining, largest)); -} - -bool WaitUntil(DinkumwareCondition* condition, DinkumwareMutex* mutex, std::chrono::system_clock::time_point deadline) { - for (;;) { - const DWORD milliseconds = WaitMillisecondsUntil(deadline); - if (milliseconds == 0) - return false; - if (SleepConditionVariableCS(&condition->native.value, &mutex->native.value, milliseconds) != FALSE) - return true; - const DWORD error = GetLastError(); - if (error != ERROR_TIMEOUT) - throw std::system_error(static_cast(error), std::system_category(), "_Cnd_timedwait: condition wait failed"); - if (std::chrono::system_clock::now() >= deadline) - return false; - } -} -#endif - DinkumwareMutex* ResolveMutex(void** handle, const char* function) { if (!handle) throw std::invalid_argument(std::string(function) + ": null mutex handle pointer"); @@ -219,6 +256,31 @@ int APS5_VABI _Mtx_trylock_nid_postfix(void** handle) { return threadSuccess; } +int APS5_VABI _Mtx_timedlock_nid_postfix(void** handle, const Xtime* target) { + if (!target) + APS5_INVALID_ARG_EX; + const auto deadline = XtimePoint(*target, "_Mtx_timedlock"); + auto* mutex = ResolveMutex(handle, "_Mtx_timedlock"); + const auto self = std::this_thread::get_id(); + if (mutex->owner.load(std::memory_order_acquire) == self) { + if ((mutex->type & mutexRecursive) == 0) + return threadBusy; + if (mutex->count == std::numeric_limits::max()) + throw std::overflow_error("_Mtx_timedlock: recursive lock count overflow"); + ++mutex->count; + return threadSuccess; + } + try { + if (!mutex->native.try_lock_until(deadline)) + return threadTimedout; + } catch (const std::system_error&) { + return threadTimedlockError; + } + mutex->owner.store(self, std::memory_order_release); + mutex->count = 1; + return threadSuccess; +} + int APS5_VABI _Mtx_unlock_nid_postfix(void** handle) { auto* mutex = ResolveMutex(handle, "_Mtx_unlock"); if (!IsOwnedByCaller(mutex)) @@ -267,12 +329,7 @@ int APS5_VABI _Cnd_wait_nid_postfix(void** conditionHandle, void** mutexHandle) mutex->owner.store(std::thread::id{}, std::memory_order_release); std::unique_lock lock(mutex->native, std::adopt_lock); try { -#ifdef _WIN32 - if (SleepConditionVariableCS(&condition->native.value, &mutex->native.value, INFINITE) == FALSE) - throw std::system_error(static_cast(GetLastError()), std::system_category(), "_Cnd_wait: condition wait failed"); -#else condition->native.wait(lock); -#endif } catch (const std::system_error&) { lock.release(); mutex->owner.store(std::this_thread::get_id(), std::memory_order_release); @@ -288,15 +345,7 @@ int APS5_VABI _Cnd_wait_nid_postfix(void** conditionHandle, void** mutexHandle) int APS5_VABI _Cnd_timedwait_nid_postfix(void** conditionHandle, void** mutexHandle, const Xtime* target) { if (!target) APS5_INVALID_ARG_EX; - const auto deadline = XtimeDeadline(*target, "_Cnd_timedwait"); - constexpr auto maxDuration = std::chrono::system_clock::duration::max(); - constexpr auto minDuration = std::chrono::system_clock::duration::min(); - constexpr auto maxSeconds = std::chrono::duration_cast(maxDuration).count(); - constexpr auto minSeconds = std::chrono::duration_cast(minDuration).count(); - constexpr auto maxNanos = std::chrono::duration_cast(maxDuration - std::chrono::seconds(maxSeconds)).count(); - if (deadline.tv_sec > maxSeconds || (deadline.tv_sec == maxSeconds && deadline.tv_nsec > maxNanos) || - deadline.tv_sec < minSeconds) - throw std::overflow_error("_Cnd_timedwait: xtime seconds overflow"); + const auto deadlineTime = XtimePoint(*target, "_Cnd_timedwait"); auto* condition = ResolveCondition(conditionHandle, "_Cnd_timedwait"); auto* mutex = ResolveMutex(mutexHandle, "_Cnd_timedwait"); if (!IsOwnedByCaller(mutex)) @@ -308,14 +357,7 @@ int APS5_VABI _Cnd_timedwait_nid_postfix(void** conditionHandle, void** mutexHan std::unique_lock lock(mutex->native, std::adopt_lock); bool timeout = false; try { - const auto deadlineTime = std::chrono::system_clock::time_point( - std::chrono::seconds(deadline.tv_sec) + - std::chrono::ceil(std::chrono::nanoseconds(deadline.tv_nsec))); -#ifdef _WIN32 - timeout = !WaitUntil(condition, mutex, deadlineTime); -#else timeout = (condition->native.wait_until(lock, deadlineTime) == std::cv_status::timeout); -#endif } catch (const std::system_error&) { lock.release(); mutex->owner.store(std::this_thread::get_id(), std::memory_order_release); @@ -329,20 +371,12 @@ int APS5_VABI _Cnd_timedwait_nid_postfix(void** conditionHandle, void** mutexHan } int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle) { -#ifdef _WIN32 - WakeAllConditionVariable(&ResolveCondition(handle, "_Cnd_broadcast")->native.value); -#else ResolveCondition(handle, "_Cnd_broadcast")->native.notify_all(); -#endif return threadSuccess; } int SignalCondition_nid_no_patch(void** handle) { -#ifdef _WIN32 - WakeConditionVariable(&ResolveCondition(handle, "_Cnd_signal")->native.value); -#else ResolveCondition(handle, "_Cnd_signal")->native.notify_one(); -#endif return threadSuccess; } @@ -464,15 +498,23 @@ void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad) { RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_unlock"); } -void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, void** thread) { +static void LaunchPad_nid_no_patch(GuestPad* pad, void** attrSlot, void** thread) { if (!pad || !thread) APS5_INVALID_ARG_EX; const auto create = registeredPthreadCreate.load(std::memory_order_acquire); if (!create) throw std::runtime_error("_Pad::_Launch: pthread_create is not registered"); - const int created = create(thread, nullptr, PadEntry_nid_no_patch, pad); + const int created = create(thread, attrSlot, PadEntry_nid_no_patch, pad); if (created != 0) ThrowCppError_nid_no_patch(6); while (!pad->started) RequireThrd(_Cnd_wait_nid_postfix(&pad->condition, &pad->mutex), "_Pad::_Launch: _Cnd_wait"); } +void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, void** thread) { + LaunchPad_nid_no_patch(pad, nullptr, thread); +} + +void APS5_VABI _ZNSt4_Pad7_LaunchEPP12pthread_attrPP7pthread_nid_postfix(GuestPad* pad, void** attrSlot, void** thread) { + LaunchPad_nid_no_patch(pad, attrSlot, thread); +} + int APS5_VABI _Thrd_join_nid_postfix(void* thread, int* code) { const auto join = registeredPthreadJoin.load(std::memory_order_acquire); if (!join) throw std::runtime_error("_Thrd_join: pthread_join is not registered"); @@ -488,11 +530,6 @@ int APS5_VABI _Thrd_detach_nid_postfix(void* thread) { return detach(thread) == 0 ? threadSuccess : threadError; } -int APS5_VABI _Thrd_id_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - [[noreturn]] void APS5_VABI _ZSt16_Throw_Cpp_errori_nid_postfix(int code) { ThrowCppError_nid_no_patch(code); } @@ -537,6 +574,10 @@ void* APS5_VABI _Thrd_current_nid_postfix(void) { return ThreadForwards(__func__).self(); } +void* APS5_VABI _Thrd_id_nid_postfix(void) { + return ThreadForwards(__func__).self(); +} + [[noreturn]] void APS5_VABI _Thrd_exit_nid_postfix(int code) { ThreadForwards(__func__).exit(reinterpret_cast(static_cast(static_cast(code)))); std::abort(); diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index ebc13dfe50..14a1b8bc68 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -21,6 +21,7 @@ #include "prx/libc/include/SonyCrt.hpp" extern "C" char* tzname_nid_postfix[2]; +extern "C" int64_t timezone_nid_postfix; namespace { @@ -243,19 +244,6 @@ constexpr const char* MONTH_FULL_NAMES[] = {"January", "February", "March", "Apr "October", "November", "December"}; constexpr int YEAR_BASE = 1900; -enum class Padding { Default, Less, Space, Zero }; - -const char* paddingFormat(int width, Padding padding, bool spaced) { - static constexpr const char* formats[][4] = { - {"%02d", "%d", "%2d", "%02d"}, - {"%2d", "%d", "%2d", "%02d"}, - {"%03d", "%d", "%3d", "%03d"}, - {"%04d", "%d", "%4d", "%04d"}, - }; - const int row = width == 2 ? (spaced ? 1 : 0) : width == 3 ? 2 : 3; - return formats[row][static_cast(padding)]; -} - bool isLeapYearSum(int year, int base) { return isLeapYear(year % 400 + base % 400); } @@ -263,248 +251,151 @@ bool isLeapYearSum(int year, int base) { struct FormatBuffer { char* cursor; char* limit; + bool full; }; -void addText(FormatBuffer& buffer, const char* text) { - while (buffer.cursor < buffer.limit && (*buffer.cursor = *text++) != '\0') ++buffer.cursor; +void addPiece(FormatBuffer& buffer, const char* text, std::size_t length) { + if (buffer.full) return; + if (length > static_cast(buffer.limit - buffer.cursor)) { + buffer.full = true; + return; + } + std::memcpy(buffer.cursor, text, length); + buffer.cursor += length; } -void addNumber(FormatBuffer& buffer, int value, const char* format) { +void addText(FormatBuffer& buffer, const char* text) { + addPiece(buffer, text, std::strlen(text)); +} + +int clampedField(std::int64_t value, int modulus) { + return value < 0 ? 0 : static_cast(value % modulus); +} + +void addNumber(FormatBuffer& buffer, std::int64_t value, int modulus, const char* format) { char text[16]; - std::snprintf(text, sizeof(text), format, value); + std::snprintf(text, sizeof(text), format, clampedField(value, modulus)); addText(buffer, text); } -void addYear(FormatBuffer& buffer, int year, int base, bool century, bool lastDigits) { - int trail = year % 100 + base % 100; - int lead = year / 100 + base / 100 + trail / 100; - trail %= 100; - if (trail < 0 && lead > 0) { - trail += 100; - --lead; - } else if (lead < 0 && trail > 0) { - trail -= 100; - ++lead; - } - if (century) { - if (lead == 0 && trail < 0) addText(buffer, "-0"); - else addNumber(buffer, lead, "%02d"); - } - if (lastDigits) addNumber(buffer, trail < 0 ? -trail : trail, "%02d"); +const char* tableName(int index, const char* const* names, const char* const* shortNames, int count) { + if (index < 0) return shortNames[0]; + return index < count ? names[index] : ""; } -const char* guestZoneName(const GuestTm& guest) { - if (guest.tm_zone != nullptr) return guest.tm_zone; - if (guest.tm_isdst < 0) return ""; -#ifdef _WIN32 - return _tzname[guest.tm_isdst != 0 ? 1 : 0]; -#else - return tzname[guest.tm_isdst != 0 ? 1 : 0]; -#endif +int twoDigitYear(std::int64_t year) { + return static_cast((year % 100 + 100) % 100); } -void addIsoWeek(FormatBuffer& buffer, char conversion, Padding padding, const GuestTm& time) { +std::int64_t weekOfYear(const GuestTm& time, int shift) { + return (static_cast(time.tm_yday) + 7 - (static_cast(time.tm_wday) + shift) % 7) / 7; +} + +struct IsoWeek { + std::int64_t year; + int week; +}; + +IsoWeek isoWeek(const GuestTm& time) { + if (time.tm_yday < 0) NotImplemented_nid_no_patch("strftime: ISO week of a negative tm_yday"); + if (time.tm_yday >= (isLeapYearSum(time.tm_year, YEAR_BASE) ? 366 : 365)) NotImplemented_nid_no_patch("strftime: ISO week of a tm_yday past the year"); + if (time.tm_wday < -6) NotImplemented_nid_no_patch("strftime: ISO week of a tm_wday below -6"); int base = YEAR_BASE; - int yday = time.tm_yday; - int week = 0; + std::int64_t yday = time.tm_yday; for (;;) { const int length = isLeapYearSum(time.tm_year, base) ? 366 : 365; - const int bottom = (yday + 11 - time.tm_wday) % 7 - 3; - int top = bottom - length % 7; + const std::int64_t bottom = (yday + 11 - time.tm_wday) % 7 - 3; + std::int64_t top = bottom - length % 7; if (top < -3) top += 7; top += length; - if (yday >= top) { - ++base; - week = 1; - break; - } - if (yday >= bottom) { - week = 1 + (yday - bottom) / 7; - break; - } + if (yday >= top) return {static_cast(time.tm_year) + base + 1, 1}; + if (yday >= bottom) return {static_cast(time.tm_year) + base, static_cast(1 + (yday - bottom) / 7)}; --base; yday += isLeapYearSum(time.tm_year, base) ? 366 : 365; } - if (conversion == 'V') addNumber(buffer, week, paddingFormat(2, padding, false)); - else addYear(buffer, time.tm_year, base, conversion == 'G', true); +} + +void formatTime(FormatBuffer& buffer, const char* format, const GuestTm& time); + +void addZoneOffset(FormatBuffer& buffer, const GuestTm& time) { + std::int64_t east = -timezone_nid_postfix + (time.tm_isdst > 0 ? 3600 : 0); + const char sign = east < 0 ? '-' : '+'; + if (east < 0) east = -east; + char text[32]; + std::snprintf(text, sizeof(text), "%c%02lld%02lld", sign, static_cast(east / 3600), static_cast(east / 60 % 60)); + addText(buffer, text); +} + +bool addConversion(FormatBuffer& buffer, char conversion, const GuestTm& time) { + const std::int64_t year = static_cast(time.tm_year) + YEAR_BASE; + switch (conversion) { + case 'a': addText(buffer, tableName(time.tm_wday, WEEKDAY_NAMES, WEEKDAY_NAMES, 7)); return true; + case 'A': addText(buffer, tableName(time.tm_wday, WEEKDAY_FULL_NAMES, WEEKDAY_NAMES, 7)); return true; + case 'b': + case 'h': addText(buffer, tableName(time.tm_mon, MONTH_NAMES, MONTH_NAMES, 12)); return true; + case 'B': addText(buffer, tableName(time.tm_mon, MONTH_FULL_NAMES, MONTH_NAMES, 12)); return true; + case 'c': formatTime(buffer, "%a %b %e %H:%M:%S %Y", time); return true; + case 'C': addNumber(buffer, time.tm_year / 100 + YEAR_BASE / 100, 100, "%02d"); return true; + case 'd': addNumber(buffer, time.tm_mday, 100, "%02d"); return true; + case 'D': + case 'x': formatTime(buffer, "%m/%d/%y", time); return true; + case 'e': addNumber(buffer, time.tm_mday, 100, "%2d"); return true; + case 'F': formatTime(buffer, "%Y-%m-%d", time); return true; + case 'g': addNumber(buffer, twoDigitYear(isoWeek(time).year), 100, "%02d"); return true; + case 'G': addNumber(buffer, isoWeek(time).year, 10000, "%04d"); return true; + case 'H': addNumber(buffer, time.tm_hour, 100, "%02d"); return true; + case 'I': addNumber(buffer, (static_cast(time.tm_hour) + 11) % 12 + 1, 100, "%02d"); return true; + case 'j': addNumber(buffer, static_cast(time.tm_yday) + 1, 1000, "%03d"); return true; + case 'm': addNumber(buffer, static_cast(time.tm_mon) + 1, 100, "%02d"); return true; + case 'M': addNumber(buffer, time.tm_min, 100, "%02d"); return true; + case 'n': addText(buffer, "\n"); return true; + case 'p': addText(buffer, time.tm_hour < 12 ? "AM" : "PM"); return true; + case 'r': formatTime(buffer, "%I:%M:%S %p", time); return true; + case 'R': formatTime(buffer, "%H:%M", time); return true; + case 'S': addNumber(buffer, time.tm_sec, 100, "%02d"); return true; + case 't': addText(buffer, "\t"); return true; + case 'T': + case 'X': formatTime(buffer, "%H:%M:%S", time); return true; + case 'u': addNumber(buffer, time.tm_wday == 0 ? 7 : time.tm_wday, 10, "%d"); return true; + case 'U': addNumber(buffer, weekOfYear(time, 7), 100, "%02d"); return true; + case 'V': addNumber(buffer, isoWeek(time).week, 100, "%02d"); return true; + case 'w': addNumber(buffer, time.tm_wday, 10, "%d"); return true; + case 'W': addNumber(buffer, weekOfYear(time, 6), 100, "%02d"); return true; + case 'y': addNumber(buffer, twoDigitYear(year), 100, "%02d"); return true; + case 'Y': addNumber(buffer, year, 10000, "%04d"); return true; + case 'z': addZoneOffset(buffer, time); return true; + case 'Z': { + const char* name = tzname_nid_postfix[time.tm_isdst > 0 ? 1 : 0]; + addText(buffer, name != nullptr ? name : ""); + return true; + } + case '%': addText(buffer, "%"); return true; + default: return false; + } } void formatTime(FormatBuffer& buffer, const char* format, const GuestTm& time) { - for (; *format != '\0'; ++format) { - if (*format == '%') { - bool alternative = false; - Padding padding = Padding::Default; - bool handled = true; - for (bool modifier = true; modifier;) { - modifier = false; - switch (*++format) { - case '\0': - --format; - handled = false; - break; - case 'A': - addText(buffer, time.tm_wday < 0 || time.tm_wday >= 7 ? "?" : WEEKDAY_FULL_NAMES[time.tm_wday]); - break; - case 'a': - addText(buffer, time.tm_wday < 0 || time.tm_wday >= 7 ? "?" : WEEKDAY_NAMES[time.tm_wday]); - break; - case 'B': - addText(buffer, time.tm_mon < 0 || time.tm_mon >= 12 ? "?" : MONTH_FULL_NAMES[time.tm_mon]); - break; - case 'b': - case 'h': - addText(buffer, time.tm_mon < 0 || time.tm_mon >= 12 ? "?" : MONTH_NAMES[time.tm_mon]); - break; - case 'C': - addYear(buffer, time.tm_year, YEAR_BASE, true, false); - break; - case 'c': - formatTime(buffer, "%a %b %e %H:%M:%S %Y", time); - break; - case 'D': - formatTime(buffer, "%m/%d/%y", time); - break; - case 'd': - addNumber(buffer, time.tm_mday, paddingFormat(2, padding, false)); - break; - case 'E': - case 'O': - if (alternative) { - handled = false; - break; - } - alternative = true; - modifier = true; - break; - case 'e': - addNumber(buffer, time.tm_mday, paddingFormat(2, padding, true)); - break; - case 'F': - formatTime(buffer, "%Y-%m-%d", time); - break; - case 'H': - addNumber(buffer, time.tm_hour, paddingFormat(2, padding, false)); - break; - case 'I': - addNumber(buffer, time.tm_hour % 12 != 0 ? time.tm_hour % 12 : 12, paddingFormat(2, padding, false)); - break; - case 'j': - addNumber(buffer, time.tm_yday + 1, paddingFormat(3, padding, false)); - break; - case 'k': - addNumber(buffer, time.tm_hour, paddingFormat(2, padding, true)); - break; - case 'l': - addNumber(buffer, time.tm_hour % 12 != 0 ? time.tm_hour % 12 : 12, paddingFormat(2, padding, true)); - break; - case 'M': - addNumber(buffer, time.tm_min, paddingFormat(2, padding, false)); - break; - case 'm': - addNumber(buffer, time.tm_mon + 1, paddingFormat(2, padding, false)); - break; - case 'n': - addText(buffer, "\n"); - break; - case 'p': - addText(buffer, time.tm_hour >= 12 ? "PM" : "AM"); - break; - case 'R': - formatTime(buffer, "%H:%M", time); - break; - case 'r': - formatTime(buffer, "%I:%M:%S %p", time); - break; - case 'S': - addNumber(buffer, time.tm_sec, paddingFormat(2, padding, false)); - break; - case 's': { - GuestTm copy = time; - char text[24]; - std::snprintf(text, sizeof(text), "%lld", static_cast(libc_mktime_nid_postfix(©))); - addText(buffer, text); - break; - } - case 'T': - formatTime(buffer, "%H:%M:%S", time); - break; - case 't': - addText(buffer, "\t"); - break; - case 'U': - addNumber(buffer, (time.tm_yday + 7 - time.tm_wday) / 7, paddingFormat(2, padding, false)); - break; - case 'u': - addNumber(buffer, time.tm_wday == 0 ? 7 : time.tm_wday, "%d"); - break; - case 'V': - case 'G': - case 'g': - addIsoWeek(buffer, *format, padding, time); - break; - case 'v': - formatTime(buffer, "%e-%b-%Y", time); - break; - case 'W': - addNumber(buffer, (time.tm_yday + 7 - (time.tm_wday != 0 ? time.tm_wday - 1 : 6)) / 7, - paddingFormat(2, padding, false)); - break; - case 'w': - addNumber(buffer, time.tm_wday, "%d"); - break; - case 'X': - formatTime(buffer, "%H:%M:%S", time); - break; - case 'x': - formatTime(buffer, "%m/%d/%y", time); - break; - case 'y': - addYear(buffer, time.tm_year, YEAR_BASE, false, true); - break; - case 'Y': - addYear(buffer, time.tm_year, YEAR_BASE, true, true); - break; - case 'Z': - addText(buffer, guestZoneName(time)); - break; - case 'z': { - if (time.tm_isdst < 0) break; - int difference = static_cast(time.tm_gmtoff); - addText(buffer, difference < 0 ? "-" : "+"); - if (difference < 0) difference = -difference; - difference /= 60; - addNumber(buffer, difference / 60 * 100 + difference % 60, paddingFormat(4, padding, false)); - break; - } - case '+': - formatTime(buffer, "%a %b %e %H:%M:%S %Z %Y", time); - break; - case '-': - case '_': - case '0': - if (padding != Padding::Default) { - handled = false; - break; - } - padding = *format == '-' ? Padding::Less : *format == '_' ? Padding::Space : Padding::Zero; - modifier = true; - break; - default: - handled = false; - break; - } - } - if (handled) continue; + for (; *format != '\0' && !buffer.full; ++format) { + if (*format != '%' || format[1] == '\0') { + addPiece(buffer, format, 1); + continue; + } + ++format; + if (*format == 'E' || *format == 'O') { + if (format[1] == '\0') throw std::invalid_argument("strftime: the format ends after an E or O modifier"); + ++format; + } + if (!addConversion(buffer, *format, time)) { + const char unknown[] = {'%', *format}; + addPiece(buffer, unknown, sizeof(unknown)); } - if (buffer.cursor == buffer.limit) break; - *buffer.cursor++ = *format; } } std::size_t formatGuestTime(char* destination, std::size_t size, const char* format, const GuestTm& time) { - FormatBuffer buffer{destination, destination + size}; + FormatBuffer buffer{destination, destination + size, false}; formatTime(buffer, format == nullptr ? "%c" : format, time); - if (buffer.cursor == buffer.limit) return 0; + if (buffer.full || buffer.cursor == buffer.limit) return 0; *buffer.cursor = '\0'; return static_cast(buffer.cursor - destination); } @@ -705,12 +596,9 @@ char* APS5_VABI asctime_nid_postfix(const GuestTm* timeptr) { std::memcpy(result, "??? ??? ?? ??:??:?? ????\n", sizeof("??? ??? ?? ??:??:?? ????\n")); return result; } - const char* weekday = timeptr->tm_wday >= 0 && timeptr->tm_wday < 7 ? WEEKDAY_NAMES[timeptr->tm_wday] : "???"; - const char* month = timeptr->tm_mon >= 0 && timeptr->tm_mon < 12 ? MONTH_NAMES[timeptr->tm_mon] : "???"; - char year[16]; - formatGuestTime(year, sizeof(year), "%Y", *timeptr); - std::snprintf(result, sizeof(result), std::strlen(year) <= 4 ? "%s %s%3d %.2d:%.2d:%.2d %-4s\n" : "%s %s%3d %.2d:%.2d:%.2d %s\n", - weekday, month, timeptr->tm_mday, timeptr->tm_hour, timeptr->tm_min, timeptr->tm_sec, year); + const std::int64_t year = static_cast(timeptr->tm_year) + YEAR_BASE; + std::snprintf(result, sizeof(result), "%s %s%3d %02d:%02d:%02d %04d\n", tableName(timeptr->tm_wday, WEEKDAY_NAMES, WEEKDAY_NAMES, 7), tableName(timeptr->tm_mon, MONTH_NAMES, MONTH_NAMES, 12), + clampedField(timeptr->tm_mday, 100), clampedField(timeptr->tm_hour, 100), clampedField(timeptr->tm_min, 100), clampedField(timeptr->tm_sec, 100), clampedField(year, 10000)); return result; } diff --git a/core/libs/prx/libc/src/WideConversion.cpp b/core/libs/prx/libc/src/WideConversion.cpp index 109dae7f16..8748715186 100644 --- a/core/libs/prx/libc/src/WideConversion.cpp +++ b/core/libs/prx/libc/src/WideConversion.cpp @@ -10,7 +10,6 @@ #include #include #include -#include extern "C" void APS5_VABI ignore_handler_s_nid_postfix(const char* message, void* pointer, int error) { SonyIgnoreHandlerS(message, pointer, error); @@ -41,8 +40,7 @@ extern "C" int APS5_VABI wcscpy_s_nid_postfix(std::uint16_t* dest, std::size_t s namespace { std::size_t ReadErrorMessage(int error, char (&message)[128]) { - const int result = strerror_r_nid_postfix(error, message, sizeof(message)); - if (result != 0 && result != 22) throw std::runtime_error("strerror_s: error message exceeds internal buffer"); + strerror_r_nid_postfix(error, message, sizeof(message)); return std::strlen(message); } @@ -81,5 +79,5 @@ extern "C" int APS5_VABI strerror_s_nid_postfix(char* buffer, std::size_t capaci std::memcpy(buffer, message, capacity - 1); buffer[capacity - 1] = '\0'; if (capacity > 3) std::memcpy(buffer + capacity - 4, "...", 3); - return 34; + return 0; } diff --git a/core/libs/prx/libc/src/exception/Runtime.cpp b/core/libs/prx/libc/src/exception/Runtime.cpp index 33a0c5485d..faba094aaf 100644 --- a/core/libs/prx/libc/src/exception/Runtime.cpp +++ b/core/libs/prx/libc/src/exception/Runtime.cpp @@ -202,12 +202,15 @@ void VisitBases(const std::type_info* type, BaseLocation location, bool publicPa void Bases(const std::type_info* type, void* object, Search& search) { std::uintptr_t identity = 0; + bool reachedPublicly = false; auto visitor = [&](const std::type_info* candidate, BaseLocation location, bool publicPath) { - if (!publicPath || !Equal(candidate, search.wanted)) return; - if (!search.count) { search.found = location.object; identity = location.identity; search.count = 1; } + if (!Equal(candidate, search.wanted)) return; + if (!search.count) { search.found = location.object; identity = location.identity; search.count = 1; reachedPublicly = publicPath; } else if (search.found != location.object || identity != location.identity) search.count = 2; + else if (publicPath) reachedPublicly = true; }; VisitBases(type, {object, 0}, true, visitor); + if (search.count == 1 && !reachedPublicly) search.count = 0; } bool PointerMatch(const __cxxabiv1::__pbase_type_info* caught, const __cxxabiv1::__pbase_type_info* thrown, diff --git a/core/libs/prx/libc/src/exception/StandardExceptions.cpp b/core/libs/prx/libc/src/exception/StandardExceptions.cpp index 2cfb9918fb..83ffdb0b78 100644 --- a/core/libs/prx/libc/src/exception/StandardExceptions.cpp +++ b/core/libs/prx/libc/src/exception/StandardExceptions.cpp @@ -237,7 +237,11 @@ static GuestLocale::String* APS5_VABI CategoryMessage_nid_no_patch(GuestLocale:: static ErrorCode APS5_VABI CategoryCondition_nid_no_patch(const CategoryObject* self, int value) { return {0, value, self}; } static bool APS5_VABI CategoryEquivalentCondition_nid_no_patch(const CategoryObject* self, int value, const ErrorCode* condition) { using Condition = ErrorCode (APS5_VABI *)(const CategoryObject*, int); +#ifdef __APPLE__ + const auto mapped = reinterpret_cast(const_cast(static_cast(self->vtable)[4]))(self, value); +#else const auto mapped = reinterpret_cast(static_cast(self->vtable)[4])(self, value); +#endif return condition->category == mapped.category && condition->value == mapped.value; } static bool APS5_VABI CategoryEquivalentCode_nid_no_patch(const CategoryObject* self, const ErrorCode* code, int value) { @@ -504,6 +508,10 @@ LibcException::TypeRecord _ZTISt14overflow_error_nid_postfix {LibcException::Sin LibcException::ExceptionVtable _ZTVSt14overflow_error_nid_postfix {0, &_ZTISt14overflow_error_nid_postfix, LibcException::GuestDestroyMessage_nid_no_patch, LibcException::GuestDeleteMessage_nid_no_patch, LibcException::GuestMessageWhat_nid_no_patch}; void APS5_VABI _ZNSt14overflow_errorD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } +LibcException::TypeRecord _ZTISt15underflow_error_nid_postfix {LibcException::SingleTypeVtable + 2, "St15underflow_error", &_ZTISt13runtime_error_nid_postfix}; +LibcException::ExceptionVtable _ZTVSt15underflow_error_nid_postfix {0, &_ZTISt15underflow_error_nid_postfix, LibcException::GuestDestroyMessage_nid_no_patch, LibcException::GuestDeleteMessage_nid_no_patch, LibcException::GuestMessageWhat_nid_no_patch}; +void APS5_VABI _ZNSt15underflow_errorD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } + LibcException::TypeRecord _ZTINSt8ios_base7failureE_nid_postfix {LibcException::SingleTypeVtable + 2, "NSt8ios_base7failureE", &_ZTISt12system_error_nid_postfix}; LibcException::ExceptionVtable _ZTVNSt8ios_base7failureE_nid_postfix {0, &_ZTINSt8ios_base7failureE_nid_postfix, LibcException::GuestDestroyMessage_nid_no_patch, LibcException::GuestDeleteMessage_nid_no_patch, LibcException::GuestMessageWhat_nid_no_patch}; void APS5_VABI _ZNSt8ios_base7failureD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } diff --git a/core/libs/prx/libc/src/exception/Unwind.cpp b/core/libs/prx/libc/src/exception/Unwind.cpp index a690410bee..973b21dbcf 100644 --- a/core/libs/prx/libc/src/exception/Unwind.cpp +++ b/core/libs/prx/libc/src/exception/Unwind.cpp @@ -107,12 +107,23 @@ extern "C" void _Unwind_DeleteException(_Unwind_Exception*); namespace LibcUnwind { #ifdef _WIN32 +static bool Readable(Word address, std::size_t size) { + for (Word cursor = address; cursor - address < size;) { + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) != sizeof(memory)) return false; + if (memory.RegionSize == 0 || memory.State != MEM_COMMIT || (memory.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0 || (memory.Protect & 0xff) == PAGE_EXECUTE) return false; + cursor = reinterpret_cast(memory.BaseAddress) + memory.RegionSize; + } + return true; +} + static Word ResolveImport(Word address) { const auto* code = reinterpret_cast(address); - if (code[0] == 0xff && code[1] == 0x25) { + if (Readable(address, 6) && code[0] == 0xff && code[1] == 0x25) { std::int32_t displacement; std::memcpy(&displacement, code + 2, sizeof(displacement)); - std::memcpy(&address, code + 6 + displacement, sizeof(address)); + const Word slot = address + 6 + displacement; + if (Readable(slot, sizeof(address))) std::memcpy(&address, reinterpret_cast(slot), sizeof(address)); } return address; } 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 c81b91a39a..0c33a3ef77 100644 --- a/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp +++ b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp @@ -1,6 +1,17 @@ -#if defined(_WIN32) || defined(__linux__) +#if defined(_WIN32) || defined(__linux__) || (defined(__APPLE__) && defined(__x86_64__)) #ifdef _WIN32 #include +#elif defined(__APPLE__) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #else #include #include @@ -14,15 +25,21 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include +#include +#include #include #include #include #include #include #include +#include #include +#include +#include extern "C" void CallTraceEnter_nid_no_patch(); @@ -71,9 +88,46 @@ bool Selected(std::string_view library) { return false; } +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); + std::vector> called; + for (std::size_t index = 0; index < recorded; ++index) { + const std::uint64_t calls = records[index].calls.load(std::memory_order_relaxed); + if (calls != 0) called.emplace_back(calls, &records[index]); + } + std::stable_sort(called.begin(), called.end(), [](const auto& left, const auto& right) { return left.first > right.first; }); + std::ofstream file(path, std::ios::binary | std::ios::trunc); + for (const auto& [calls, record] : called) file << calls << ' ' << record->library << ' ' << record->name << '\n'; + file.flush(); + return static_cast(file); +} + +void StartCallCountWriter() { +#ifdef _WIN32 + const wchar_t* value = _wgetenv(L"APS5_TRACE_CALLS_FILE"); + if (value == nullptr || *value == L'\0') return; + std::filesystem::path path(value); +#else + const char* value = std::getenv("APS5_TRACE_CALLS_FILE"); + if (value == nullptr || *value == '\0') return; + std::filesystem::path path(value); +#endif + std::thread([path = std::move(path)] { + for (;;) { + std::this_thread::sleep_for(std::chrono::seconds(10)); + WriteCallCounts(path); + } + }).detach(); +} + std::uint64_t CurrentThread() { #ifdef _WIN32 return GetCurrentThreadId(); +#elif defined(__APPLE__) + std::uint64_t thread = 0; + pthread_threadid_np(nullptr, &thread); + return thread; #else return static_cast(gettid()); #endif @@ -89,6 +143,22 @@ void MapThunkArea() { CloseHandle(section); if (g_writable == nullptr || g_executable == nullptr) throw std::system_error(static_cast(code), std::system_category(), "call trace: MapViewOfFile"); } +#elif defined(__APPLE__) +void MapThunkArea() { + void* writable = mmap(nullptr, ThunkAreaBytes, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0); + if (writable == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "call trace: mmap"); + mach_vm_address_t executable = 0; + vm_prot_t current = VM_PROT_NONE; + vm_prot_t maximum = VM_PROT_NONE; + const kern_return_t remapped = mach_vm_remap(mach_task_self(), &executable, ThunkAreaBytes, 0, VM_FLAGS_ANYWHERE, mach_task_self(), reinterpret_cast(writable), false, ¤t, &maximum, VM_INHERIT_NONE); + if (remapped != KERN_SUCCESS || mach_vm_protect(mach_task_self(), executable, ThunkAreaBytes, false, VM_PROT_READ | VM_PROT_EXECUTE) != KERN_SUCCESS) { + if (remapped == KERN_SUCCESS) mach_vm_deallocate(mach_task_self(), executable, ThunkAreaBytes); + munmap(writable, ThunkAreaBytes); + throw std::system_error(remapped == KERN_SUCCESS ? KERN_PROTECTION_FAILURE : remapped, std::system_category(), "call trace: mach_vm_remap"); + } + g_writable = static_cast(writable); + g_executable = reinterpret_cast(executable); +} #else void MapThunkArea() { const int file = memfd_create("aps5-call-trace", MFD_CLOEXEC); @@ -108,7 +178,9 @@ void MapThunkArea() { g_writable = static_cast(writable); g_executable = static_cast(executable); } +#endif +#ifndef _WIN32 struct ModuleSegment { std::uint64_t start; std::uint64_t end; @@ -128,6 +200,42 @@ std::atomic g_reporting{false}; std::atomic g_reported{false}; unsigned long long g_moduleChanges = ~0ull; +#if defined(__APPLE__) +void SnapshotModules() { + const auto images = _dyld_image_count(); + unsigned long long changes = images; + for (std::uint32_t index = 0; index < images; ++index) + changes = changes * 131u + reinterpret_cast(_dyld_get_image_header(index)); + if (changes == g_moduleChanges) return; + g_moduleChanges = changes; + auto* map = new ModuleMap{}; + for (std::uint32_t index = 0; index < images && map->count < std::size(map->segments); ++index) { + const auto* header = reinterpret_cast(_dyld_get_image_header(index)); + if (header == nullptr || header->magic != MH_MAGIC_64) continue; + const char* path = _dyld_get_image_name(index); + if (path == nullptr || *path == '\0') path = getprogname(); + const char* slash = std::strrchr(path, '/'); + const char* name = slash == nullptr ? path : slash + 1; + const auto slide = static_cast(_dyld_get_image_vmaddr_slide(index)); + const auto base = reinterpret_cast(header); + auto* command = reinterpret_cast(header + 1); + for (std::uint32_t commandIndex = 0; commandIndex < header->ncmds && map->count < std::size(map->segments); ++commandIndex) { + if (command->cmd == LC_SEGMENT_64) { + const auto* segment = reinterpret_cast(command); + if (segment->vmsize != 0 && (segment->initprot & VM_PROT_EXECUTE) != 0) { + auto& recorded = map->segments[map->count++]; + recorded.base = base; + recorded.start = segment->vmaddr + slide; + recorded.end = recorded.start + segment->vmsize; + std::strncpy(recorded.name, name, sizeof(recorded.name) - 1); + } + } + command = reinterpret_cast(reinterpret_cast(command) + command->cmdsize); + } + } + g_modules.store(map, std::memory_order_release); +} +#else void SnapshotModules() { unsigned long long changes = 0; dl_iterate_phdr([](dl_phdr_info* info, std::size_t, void* data) { @@ -159,6 +267,7 @@ void SnapshotModules() { }, map); g_modules.store(map, std::memory_order_release); } +#endif class ReportLine { public: @@ -331,11 +440,12 @@ void* TraceImport_nid_no_patch(void* target, const char* name, const char* libra MapThunkArea(); g_records.store(new ImportRecord[RecordCapacity], std::memory_order_release); g_ring.store(new RingSlot[RingCapacity], std::memory_order_release); -#ifdef __linux__ +#if defined(__linux__) || defined(__APPLE__) InstallAbortReport(); #endif + StartCallCountWriter(); } -#ifdef __linux__ +#if defined(__linux__) || defined(__APPLE__) SnapshotModules(); #endif if (const auto existing = g_thunkByTarget.find(target); existing != g_thunkByTarget.end()) return existing->second; @@ -386,6 +496,11 @@ std::size_t CopyThreadCallTrace_nid_no_patch(std::uint64_t thread, CallTraceEntr return count; } +bool WriteCallCounts_nid_no_patch(const char* path) { + if (path == nullptr || *path == '\0') return false; + return WriteCallCounts(std::filesystem::path(reinterpret_cast(path))); +} + std::size_t CopyCallCounts_nid_no_patch(CallTraceCount* entries, std::size_t capacity, std::uint64_t* total) { if (total != nullptr) *total = g_sequence.load(std::memory_order_acquire); if (entries == nullptr || capacity == 0) return 0; @@ -430,7 +545,7 @@ asm(".text\n" " movdqu %xmm7, 112(%rsp)\n" " mov %r11, %rdi\n" " mov 200(%rsp), %rsi\n" - " call CallTraceRecord_nid_no_patch\n" + " call " APS5_ASM_SYMBOL("CallTraceRecord_nid_no_patch") "\n" " movdqu 0(%rsp), %xmm0\n" " movdqu 16(%rsp), %xmm1\n" " movdqu 32(%rsp), %xmm2\n" diff --git a/core/libs/prx/libkernel/Aio/src/Aio.cpp b/core/libs/prx/libkernel/Aio/src/Aio.cpp index 54fb54921f..556649a58b 100644 --- a/core/libs/prx/libkernel/Aio/src/Aio.cpp +++ b/core/libs/prx/libkernel/Aio/src/Aio.cpp @@ -11,6 +11,7 @@ #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/PreciseWait.hpp" #include "prx/libkernel/File/include/ArchivedFile.hpp" +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" @@ -88,6 +89,7 @@ std::int32_t DeleteLocked(std::int32_t id) { } std::int64_t NativePread(std::int32_t fd, void* buf, std::size_t nbyte, std::int64_t offset) { + if (File::IsVirtualDirectoryDescriptor(fd)) { errno = EISDIR; return -1; } const GuestArena::HostWrite destination(buf, nbyte); if (!destination.Open()) { errno = EFAULT; @@ -110,6 +112,7 @@ std::int64_t NativePread(std::int32_t fd, void* buf, std::size_t nbyte, std::int } std::int64_t NativePwrite(std::int32_t fd, const void* buf, std::size_t nbyte, std::int64_t offset) { + if (File::IsVirtualDirectoryDescriptor(fd)) { errno = EISDIR; return -1; } if (offset < 0) { errno = EINVAL; return -1; @@ -133,6 +136,7 @@ void RunRequest(KernelAioRwRequest& req, bool write) { if (error == EBADF) req.result->return_value = static_cast(SCE_KERNEL_ERROR_EBADF); if (error == EFAULT) req.result->return_value = static_cast(SCE_KERNEL_ERROR_EFAULT); if (error == EINVAL) req.result->return_value = static_cast(SCE_KERNEL_ERROR_EINVAL); + if (error == EISDIR) req.result->return_value = static_cast(SCE_KERNEL_ERROR_EISDIR); req.result->state = AioCompleted; return; } diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 92cbd44e5e..dc0bf60787 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -56,6 +56,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Module/src/Module.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Module/src/DynamicLoader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Module/src/ModuleInfo.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Module/src/Tls.cpp ${LIBS_INCLUDE_DIR}/nid/src/NidCompute.cpp ${LIBS_INCLUDE_DIR}/nid/src/Sha1.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Aio/src/Aio.cpp @@ -68,6 +69,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/System/src/HostCpu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SystemConfiguration.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Login.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Getlogin.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SoftwareVersion.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Error.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/ThermalSensors.cpp @@ -125,6 +127,20 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_library(guest_exec_host SHARED ${LIBS_INCLUDE_DIR}/tests/GuestExecHost.cpp) + target_include_directories(guest_exec_host PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_exec_host PRIVATE libkernel libc libSceAgcDriver) + set_target_properties(guest_exec_host PROPERTIES PREFIX "" SUFFIX ".prx" BUILD_RPATH "$ORIGIN") + configure_windows_unwind(guest_exec_host) + find_package(Python3 COMPONENTS Interpreter QUIET) + if(Python3_FOUND) + add_test(NAME exec_arguments COMMAND ${Python3_EXECUTABLE} + ${CMAKE_SOURCE_DIR}/core/relinker/relinker/tests/test_exec_arguments.py + $ $ + $ $ $ ${CMAKE_CXX_COMPILER}) + set_tests_properties(exec_arguments PROPERTIES TIMEOUT 30) + add_asan_test_options(exec_arguments verify_asan_link_order=0) + endif() if(NOT WIN32) add_library(guest_memory_lifetime_module MODULE ${LIBS_INCLUDE_DIR}/tests/GuestMemoryLifetimeModule.cpp) target_include_directories(guest_memory_lifetime_module PRIVATE ${LIBS_INCLUDE_DIR}) @@ -564,6 +580,16 @@ if(BUILD_TESTING) set_tests_properties(guest_kernel_stack_protect PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_tls_get_addr_tests ${LIBS_INCLUDE_DIR}/tests/GuestTlsGetAddr.cpp) + target_include_directories(guest_tls_get_addr_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_tls_get_addr_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_tls_get_addr_tests) + add_test(NAME guest_tls_get_addr COMMAND guest_tls_get_addr_tests) + set_tests_properties(guest_tls_get_addr PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_tls_get_addr PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_thread_self_tests ${LIBS_INCLUDE_DIR}/tests/GuestThreadSelf.cpp) target_include_directories(guest_thread_self_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_thread_self_tests PRIVATE libkernel libc) @@ -731,6 +757,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_condattr PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_cond_setname_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadCondSetname.cpp) + target_include_directories(guest_pthread_cond_setname_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_cond_setname_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_cond_setname_tests) + add_test(NAME guest_pthread_cond_setname COMMAND guest_pthread_cond_setname_tests) + set_tests_properties(guest_pthread_cond_setname PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_cond_setname PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_cond_recursive_mutex_tests ${LIBS_INCLUDE_DIR}/tests/GuestCondRecursiveMutex.cpp) target_include_directories(guest_cond_recursive_mutex_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_cond_recursive_mutex_tests PRIVATE libkernel libc) @@ -990,10 +1026,30 @@ if(BUILD_TESTING) target_link_libraries(guest_signal_tests PRIVATE libkernel libc) configure_windows_unwind(guest_signal_tests) add_test(NAME guest_signals COMMAND guest_signal_tests) + add_test_executable(guest_signal_interrupt_tests ${LIBS_INCLUDE_DIR}/tests/GuestSignalInterrupt.cpp) + target_include_directories(guest_signal_interrupt_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_signal_interrupt_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_signal_interrupt_tests) + add_test(NAME guest_signal_interrupt COMMAND guest_signal_interrupt_tests) + set_tests_properties(guest_signal_interrupt PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_signal_interrupt PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() if(WIN32) set_tests_properties(guest_signals PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") 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}) + target_link_libraries(guest_signal_handler_mask_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_signal_handler_mask_tests) + add_test(NAME guest_signal_handler_mask COMMAND guest_signal_handler_mask_tests) + set_tests_properties(guest_signal_handler_mask PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_signal_handler_mask PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_sigtimedwait_tests ${LIBS_INCLUDE_DIR}/tests/GuestSigtimedwait.cpp) target_include_directories(guest_sigtimedwait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_sigtimedwait_tests PRIVATE libkernel libc) @@ -1183,7 +1239,7 @@ if(BUILD_TESTING) endif() add_test_executable(guest_getlogin_tests ${LIBS_INCLUDE_DIR}/tests/GuestGetlogin.cpp) target_include_directories(guest_getlogin_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_getlogin_tests PRIVATE libkernel libc) + target_link_libraries(guest_getlogin_tests PRIVATE libkernel libkernel_sys libc) configure_windows_unwind(guest_getlogin_tests) add_test(NAME guest_getlogin COMMAND guest_getlogin_tests) set_tests_properties(guest_getlogin PROPERTIES TIMEOUT 20) @@ -1545,7 +1601,7 @@ if(BUILD_TESTING) endif() add_test_executable(guest_kernel_app_archive_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelAppArchive.cpp) target_include_directories(guest_kernel_app_archive_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_kernel_app_archive_tests PRIVATE libkernel libc zarchive) + target_link_libraries(guest_kernel_app_archive_tests PRIVATE libSceLibcInternal libkernel libc zarchive) configure_windows_unwind(guest_kernel_app_archive_tests) foreach(mode archive plain mmap) add_test(NAME guest_kernel_app_archive_${mode} COMMAND guest_kernel_app_archive_tests ${mode}) @@ -1779,6 +1835,13 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() if(WIN32) + add_test_executable(guest_windows_directory_link_types_tests ${LIBS_INCLUDE_DIR}/tests/GuestWindowsDirectoryLinkTypes.cpp) + target_include_directories(guest_windows_directory_link_types_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_windows_directory_link_types_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_windows_directory_link_types_tests) + add_test(NAME guest_windows_directory_link_types COMMAND guest_windows_directory_link_types_tests) + set_tests_properties(guest_windows_directory_link_types PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") add_test_executable(guest_directory_buffer_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryBuffer.cpp) target_include_directories(guest_directory_buffer_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_directory_buffer_tests PRIVATE libkernel libc) @@ -1926,6 +1989,16 @@ if(BUILD_TESTING) configure_windows_unwind(guest_append_writes_tests) add_test(NAME guest_append_writes COMMAND guest_append_writes_tests) set_tests_properties(guest_append_writes PROPERTIES TIMEOUT 20) + add_test_executable(guest_kernel_fcntl_sync_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelFcntlSync.cpp) + target_include_directories(guest_kernel_fcntl_sync_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_kernel_fcntl_sync_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_kernel_fcntl_sync_tests) + add_test(NAME guest_kernel_fcntl_sync COMMAND guest_kernel_fcntl_sync_tests) + set_tests_properties(guest_kernel_fcntl_sync PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(guest_kernel_fcntl_sync PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() if(WIN32) set_tests_properties(guest_kernel_fcntl guest_append_writes PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") @@ -1962,6 +2035,11 @@ if(BUILD_TESTING) add_test(NAME guest_raise_in_system_call COMMAND guest_raise_in_system_call_tests) set_tests_properties(guest_raise_in_system_call PROPERTIES TIMEOUT 30 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + foreach(delay IN ITEMS raise return) + add_test(NAME guest_raise_in_system_call_delay_${delay} COMMAND guest_raise_in_system_call_tests --delay-${delay}) + set_tests_properties(guest_raise_in_system_call_delay_${delay} PROPERTIES TIMEOUT 30 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endforeach() add_test_executable(guest_raise_in_host_code_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseInHostCode.cpp) target_include_directories(guest_raise_in_host_code_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_raise_in_host_code_tests PRIVATE libkernel libc) @@ -2014,6 +2092,15 @@ if(BUILD_TESTING) target_include_directories(guest_timed_join_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_timed_join_tests PRIVATE libkernel libc ${CMAKE_DL_LIBS}) configure_windows_unwind(guest_timed_join_tests) + if(WIN32 AND Python3_Interpreter_FOUND) + foreach(joinProvider IN ITEMS libkernel_sys libkernel_web) + add_test(NAME guest_timed_join_import_${joinProvider} COMMAND ${Python3_EXECUTABLE} + ${LIBS_INCLUDE_DIR}/tests/test_windows_timed_join_imports.py $ ${PATCHED_DIR} + ${joinProvider}.prx $) + set_tests_properties(guest_timed_join_import_${joinProvider} PROPERTIES TIMEOUT 25 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endforeach() + endif() foreach(joinMode IN ITEMS normal coarse) add_test(NAME guest_timed_join_${joinMode} COMMAND guest_timed_join_tests $) set_tests_properties(guest_timed_join_${joinMode} PROPERTIES TIMEOUT 15) diff --git a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp index 24ad4c0331..2f5476c77c 100644 --- a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp @@ -6,6 +6,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/File/include/ArchivedFile.hpp" +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libkernel/File/include/SharedMemory.hpp" #include #include @@ -19,7 +20,6 @@ #include #elif defined(_WIN32) #include "prx/libc/include/WindowsDescriptor.hpp" -#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" #endif @@ -188,6 +188,7 @@ void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protecti (mappingFlags & ~(GuestShared | GuestPrivate | GuestMapFixed | GuestMapNoOverwrite | GuestMapNoCoalesce)) != 0) return failed(GuestNotSupported); if (descriptor < 0) return failed(GuestBadDescriptor); + if (File::IsVirtualDirectoryDescriptor(descriptor)) return failed(GuestNotSupported); File::RefuseArchivedFile(descriptor, "mmap"); if (offset < 0 || (static_cast(offset) & (PS5_PAGE_SIZE - 1)) != 0 || rounded > static_cast(std::numeric_limits::max()) || diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index f4ad697406..059f00798c 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -287,6 +287,10 @@ int KernelEqueuePrivate::CollectTriggered(KernelEvent* ev, int num) { while (e.triggered && ret < num) { ev[ret++] = e.event; if ((e.event.flags & EV_ONESHOT) != 0) { + if (e.filter.deleteEventFunc != nullptr) { + auto owner = e.filter.owner; + e.filter.deleteEventFunc(m_handle, &e); + } erase = true; break; } @@ -354,9 +358,7 @@ int KernelEqueuePrivate::CollectOncePerWait(KernelEvent* ev, int num) { e.event.data = 0; } const auto next = std::next(it); - if (e.triggered) { - requeued.splice(requeued.end(), m_events, it); - } + requeued.splice(requeued.end(), m_events, it); it = next; } m_events.splice(m_events.end(), requeued); diff --git a/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp b/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp index 9065ffd156..310bfd239e 100644 --- a/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp +++ b/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp @@ -25,10 +25,7 @@ int APS5_VABI sceKernelGetEventError(const KernelEvent* ev) { if (event.fflags == 0) { return 0; } - if (event.fflags > 0xFFFFu) { - throw std::runtime_error(std::string(__func__) + ": fflags " + std::to_string(event.fflags) + " above 0xFFFF"); - } - return static_cast(0x80020000u | event.fflags); + return static_cast(0x80020000u + static_cast(event.fflags)); } intptr_t APS5_VABI sceKernelGetEventFflags(const KernelEvent* ev) { diff --git a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp index 084de00999..a0b5e49fd3 100644 --- a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp +++ b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp @@ -193,7 +193,7 @@ bool TakeSnapshot(HANDLE handle, Snapshot& snapshot) { } bool IsDeleted(const Snapshot& snapshot) { - return snapshot.deletePending || snapshot.links == 0; + return snapshot.deletePending || snapshot.links == 0 || snapshot.path.find(L"\\$Extend\\$Deleted\\") != std::wstring::npos; } std::vector SnapshotHints(const Snapshot& before, const Snapshot& after, bool directory) { @@ -213,7 +213,7 @@ std::vector SnapshotHints(const Snapshot& before, const Snapshot& } else if (!directory && hints.empty() && after.change != before.change) { hints.push_back(NOTE_ATTRIB); } - if ((after.deletePending && !before.deletePending) || after.links < before.links) { + if ((after.deletePending && !before.deletePending) || after.links < before.links || (!IsDeleted(before) && IsDeleted(after))) { hints.push_back(NOTE_DELETE); } if (!after.path.empty() && after.path != before.path && !IsDeleted(after)) { diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp index 66a863d6c7..cbddf855ce 100644 --- a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -401,7 +401,7 @@ int ModifyTimerKnote(const KernelEqueueRef& queue, KernelEvent& change) { } int ModifyKnote(const KernelEqueueRef& queue, KernelEvent& change) { - if (change.filter == EVFILT_TIMER) { + if (change.filter == EVFILT_TIMER && ((change.flags & EV_ADD) != 0 || change.data != 0 || change.fflags != 0)) { NotImplemented_nid_no_patch("kevent: changing an existing EVFILT_TIMER"); } const bool descriptor = change.filter == EVFILT_READ || change.filter == EVFILT_WRITE; diff --git a/core/libs/prx/libkernel/File/include/ArchivedFile.hpp b/core/libs/prx/libkernel/File/include/ArchivedFile.hpp index 73f84c1cca..fa0f089d47 100644 --- a/core/libs/prx/libkernel/File/include/ArchivedFile.hpp +++ b/core/libs/prx/libkernel/File/include/ArchivedFile.hpp @@ -11,7 +11,6 @@ namespace File { std::optional OpenArchivedFile(const std::filesystem::path& native, int flags); -void RefuseMergedDirectory(const std::filesystem::path& native); bool IsArchivedFile(int fd); void RefuseArchivedFile(int fd, const char* operation); std::optional ReadArchivedFile(int fd, void* buf, std::size_t nbytes, const std::int64_t* offset); diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp index e927284888..f2555a2dae 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBKERNEL_FILE_DESCRIPTORCONTROL_HPP #include +#include namespace File { @@ -17,8 +18,22 @@ void RememberPipe(int fd); void ShareDescriptor(int from, int to); void ForgetDescriptor(int fd); bool Appends(int fd); +void SyncWrites(int fd); + +class WriteSync { +public: + explicit WriteSync(int fd) : _fd(fd), _exceptions(std::uncaught_exceptions()) {} + ~WriteSync() noexcept(false) { + if (std::uncaught_exceptions() == _exceptions) SyncWrites(_fd); + } + +private: + int _fd; + int _exceptions; +}; bool IsVnode(int fd); bool IsCharacterDevice(int fd); +bool GrantsAccess(int fd, bool write); } diff --git a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp index 6978a7998a..f3f16f750b 100644 --- a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp +++ b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp @@ -4,11 +4,17 @@ #include #include #include +#include "prx/libc/include/general/VabiMacros.hpp" struct FileStat; extern "C" const char* GetDirectoryDescriptorPath_nid_no_patch(int fd, int* error); extern "C" std::uint64_t GetDirectoryDescriptorPosition_nid_no_patch(int fd); +extern "C" int IsDirectoryDescriptor_nid_no_patch(int fd); +extern "C" int ReadDirectoryDescriptor_nid_no_patch(int fd, char* buf, int nbytes); +extern "C" std::int64_t SeekDirectoryDescriptor_nid_no_patch(int fd, std::int64_t offset, int whence); +extern "C" int APS5_VABI ReadDirectoryDescriptorCallback_nid_no_patch(int fd, char* buf, int nbytes); +extern "C" std::int64_t APS5_VABI SeekDirectoryDescriptorCallback_nid_no_patch(int fd, std::int64_t offset, int whence); extern "C" int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std::uint16_t mode); extern "C" int StatInDirectory_nid_no_patch(int directory, const char* path, FileStat* sb, bool follow); @@ -16,6 +22,8 @@ namespace File { int OpenDirectoryDescriptor(const std::filesystem::path& path); bool IsDirectoryDescriptor(int fd); +bool IsVirtualDirectoryDescriptor(int fd); +bool IsNativeDirectoryDescriptor(int fd); std::optional DirectoryDescriptorPath(int fd); void ForgetDirectoryDescriptor(int fd); void DuplicateDirectoryDescriptor(int from, int to); diff --git a/core/libs/prx/libkernel/File/src/ArchivedFile.cpp b/core/libs/prx/libkernel/File/src/ArchivedFile.cpp index df3a8548b4..2f1a17cc66 100644 --- a/core/libs/prx/libkernel/File/src/ArchivedFile.cpp +++ b/core/libs/prx/libkernel/File/src/ArchivedFile.cpp @@ -55,12 +55,6 @@ std::optional OpenArchivedFile(const std::filesystem::path& native, int fla return fd; } -void RefuseMergedDirectory(const std::filesystem::path& native) { - if (!AppArchiveHasDirectory_nid_no_patch(native)) return; - std::error_code error; - if (std::filesystem::is_directory(native, error)) NotImplemented_nid_no_patch(("opening " + native.string() + " merged with app0.zar").c_str()); -} - bool IsArchivedFile(int fd) { return AppArchiveDescriptor_nid_no_patch(fd, nullptr, nullptr); } diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index 34b7fe576a..7894f301c2 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -378,7 +378,6 @@ int ChangeStatus(int fd, Kind kind, int mask, int value, const char* function) { const int next = (status & ~mask) | (value & mask); const int changed = status ^ next; if ((changed & Asynchronous) && (kind == Kind::Pipe || kind == Kind::Socket || kind == Kind::Device)) Unsupported(function, "O_ASYNC", kind, fd); - if ((changed & Synchronous) && kind == Kind::File) Unsupported(function, "changing O_FSYNC", kind, fd); ApplyHostStatus(fd, kind, changed, next, function); status = next; return 0; @@ -401,6 +400,7 @@ void SetCloseOnExec(int fd, bool value) { } int RecordLock(int fd, int command, std::intptr_t argument) { + if (File::IsVirtualDirectoryDescriptor(fd)) return Fail(GuestEinval); const bool old = command <= OldSetLockWait; const int current = old ? command + (GetLock - OldGetLock) : command; const std::size_t size = old ? OldGuestLockSize : sizeof(GuestLock); @@ -556,6 +556,7 @@ bool IoctlRequestMalformed(std::uint64_t request) { int Ioctl(int fd, std::uint64_t request, void* argument) { if (IoctlRequestMalformed(request)) return Fail(GuestEnotty); + if (File::IsVirtualDirectoryDescriptor(fd)) return Fail(GuestEinval); const auto command = static_cast(request); const std::uint32_t length = (command >> 16) & 0x1fff; if ((command & RequestIn) && length != 0 && argument == nullptr) return Fail(GuestEfault); @@ -617,6 +618,21 @@ void ForgetDescriptor(int fd) { g_entries.erase(fd); } +std::uint64_t g_synchronousWrites = 0; + +void SyncWrites(int fd) { + { + std::lock_guard lock(g_mutex); + const auto found = g_entries.find(fd); + if (found == g_entries.end() || !found->second.status || (*found->second.status & Synchronous) == 0) return; + } + if (Classify(fd, "write") != Kind::File) return; + if (const int result = sceKernelFsync(fd); result != 0) + throw std::runtime_error("write: syncing an O_FSYNC file failed, fd=" + std::to_string(fd) + ", error=" + std::to_string(result & 0xffff)); + std::lock_guard lock(g_mutex); + ++g_synchronousWrites; +} + bool Appends(int fd) { std::lock_guard lock(g_mutex); const auto found = g_entries.find(fd); @@ -633,6 +649,11 @@ bool IsCharacterDevice(int fd) { #endif } +bool GrantsAccess(int fd, bool write) { + const int access = AccessMode(fd, write ? "pwrite" : "pread"); + return access == ReadWrite || access == (write ? WriteOnly : ReadOnly); +} + bool IsVnode(int fd) { if (fd < 0 || File::IsRandomDevice(fd)) return false; if (File::IsArchivedFile(fd)) return true; @@ -650,3 +671,8 @@ bool IsVnode(int fd) { extern "C" bool DescriptorHasNoVnode_nid_no_patch(int fd) { return !File::IsVnode(fd) && !File::IsCharacterDevice(fd) && !File::IsRandomDevice(fd); } + +extern "C" std::uint64_t FileSynchronousWritesForTest_nid_no_patch() { + std::lock_guard lock(g_mutex); + return File::g_synchronousWrites; +} diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index e140a6b0f4..11002b4f93 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -1,41 +1,62 @@ +#include "prx/libc/include/GuestFileNumber.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libkernel/KernelErrors.hpp" +#include "prx/libc/include/AppArchive.hpp" +#include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/HostThreadLocal.hpp" -#include -#include - -#ifdef _WIN32 - -#include "prx/libc/include/FileStream.hpp" -#include "prx/libc/include/General.hpp" - -#include -#include +#include #include +#include #include #include -#include -#include -#include #include #include #include #include -#include #include +#include #include #include +#ifdef _WIN32 +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#else +#include +#include +#include +#endif + namespace { +constexpr std::uint8_t GuestUnknownType = 0; constexpr std::uint8_t GuestDirectoryType = 4; constexpr std::uint8_t GuestRegularType = 8; +constexpr std::uint8_t GuestSymbolicLinkType = 10; + +struct DirectoryEntry { + std::string name; + std::uint32_t fileNumber; + std::uint8_t type; +}; + +class DirectoryEntryNameTooLong : public std::runtime_error { +public: + DirectoryEntryNameTooLong() : std::runtime_error("Directory entry name exceeds the guest 255-byte limit") {} +}; struct DirectoryState { std::mutex mutex; - std::vector> entries; + std::filesystem::path path; + bool virtualNamespace = false; + bool nativeBacked = false; + std::vector entries; std::size_t cursor = 0; bool loaded = false; }; @@ -44,26 +65,172 @@ std::mutex g_mutex; std::map> g_directories; std::atomic g_directoryCount{0}; +int DirectoryError(const std::error_code& code) { +#ifdef _WIN32 + if (code.category() == std::system_category()) + return FilesystemError(std::error_code(WindowsOpenError_nid_no_patch(static_cast(code.value())), std::generic_category())); +#endif + return FilesystemError(code); +} + std::shared_ptr Find(int fd) { + if (g_directoryCount.load(std::memory_order_relaxed) == 0) return nullptr; std::lock_guard lock(g_mutex); const auto found = g_directories.find(fd); return found == g_directories.end() ? nullptr : found->second; } -void Load(DirectoryState& state, int fd) { - if (state.loaded) return; - const auto path = File::WindowsDescriptorPath_nid_no_patch(fd); - if (!path) throw std::system_error(errno, std::generic_category(), "directory descriptor path"); - state.loaded = true; - state.entries.emplace_back(".", GuestDirectoryType); - state.entries.emplace_back("..", GuestDirectoryType); - std::error_code error; - for (const auto& entry : std::filesystem::directory_iterator(*path, error)) { - std::error_code typeError; - state.entries.emplace_back(entry.path().filename().string(), entry.is_directory(typeError) ? GuestDirectoryType : GuestRegularType); +#ifndef _WIN32 +std::uint8_t DirectoryEntryType(const std::filesystem::file_status& status) { + switch (status.type()) { + case std::filesystem::file_type::regular: return GuestRegularType; + case std::filesystem::file_type::directory: return GuestDirectoryType; + case std::filesystem::file_type::symlink: return GuestSymbolicLinkType; + case std::filesystem::file_type::block: return 6; + case std::filesystem::file_type::character: return 2; + case std::filesystem::file_type::fifo: return 1; + case std::filesystem::file_type::socket: return 12; + default: return 0; } } +#else +bool InsideApp0(const std::filesystem::path& directory) { + const auto root = ResolveDiskPath_nid_no_patch("/app0"); + const auto handle = CreateFileW(root.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 false; + const int descriptor = ::_open_osfhandle(reinterpret_cast(handle), _O_RDONLY | _O_BINARY); + if (descriptor < 0) { + CloseHandle(handle); + return false; + } + const auto app0 = File::WindowsDescriptorPath_nid_no_patch(descriptor); + ::_close(descriptor); + if (!app0) return false; + auto entry = directory.begin(); + for (const auto& component : *app0) { + if (entry == directory.end()) return false; + const auto& left = entry->native(); + const auto& right = component.native(); + if (CompareStringOrdinal(left.c_str(), static_cast(left.size()), + right.c_str(), static_cast(right.size()), TRUE) != CSTR_EQUAL) return false; + ++entry; + } + return true; +} +std::uint8_t EntryType(const std::filesystem::path& path, bool followLinks) { + FileStat status{}; + if (!File::FillLinkStat(path, &status)) + return GuestUnknownType; + if (followLinks && (status.st_mode & 0170000) == 0120000) { + FileStat target{}; + const int savedErrno = errno; + const bool found = File::TryFillFileStat(path, &target); + errno = savedErrno; + if (found && ((target.st_mode & 0170000) == 0040000 || (target.st_mode & 0170000) == 0100000)) + status = target; + } + switch (status.st_mode & 0170000) { + case 0040000: return GuestDirectoryType; + case 0100000: return GuestRegularType; + case 0120000: return GuestSymbolicLinkType; + default: NotImplemented_nid_no_patch("getdents of a Windows file type other than a directory, regular file or symbolic link"); + } +} +#endif + +void Add(DirectoryState& state, const std::string& name, std::uint64_t fileNumber, std::uint8_t type) { + if (name.size() > 255) throw DirectoryEntryNameTooLong(); + state.entries.push_back({name, GuestFileNumber_nid_no_patch(fileNumber), type}); +} + +void Load(DirectoryState& state, int fd) { + if (state.loaded) return; + state.entries.clear(); + const auto directoryPath = state.nativeBacked ? File::DirectoryDescriptorPath(fd).value_or(state.path) : state.path; + AppArchiveNode node{}; + const bool hasArchiveDirectory = AppArchiveLookup_nid_no_patch(directoryPath, &node) && node.Directory; + std::error_code hostError; + const auto hostPathStatus = std::filesystem::status(directoryPath, hostError); + if (hostError && hostError != std::errc::no_such_file_or_directory) + throw std::system_error(hostError, "stat directory descriptor path"); + const bool hostPathExists = !hostError && std::filesystem::exists(hostPathStatus); + const bool hasHostDirectory = hostPathExists && std::filesystem::is_directory(hostPathStatus); + FileStat directoryStatus{}; + if (hasHostDirectory && !File::TryFillFileStat(directoryPath, &directoryStatus)) + throw std::system_error(errno, std::generic_category(), "stat directory descriptor identity"); + if (!hasHostDirectory && !hasArchiveDirectory) throw std::system_error(ENOTDIR, std::generic_category(), "directory descriptor path"); + std::uint64_t currentNumber = hasArchiveDirectory ? static_cast(node.Handle) + 1 : 0; + if (hasHostDirectory) currentNumber = static_cast(directoryStatus.st_ino); + Add(state, ".", currentNumber, GuestDirectoryType); + std::uint64_t parentNumber = 0; + FileStat parentStatus{}; + const bool hasHostParent = File::TryFillFileStat(directoryPath.parent_path(), &parentStatus); + if (hasHostParent) parentNumber = static_cast(parentStatus.st_ino); + AppArchiveNode parentNode{}; + if (!hasHostParent && AppArchiveLookup_nid_no_patch(directoryPath.parent_path(), &parentNode) && parentNode.Directory) + parentNumber = static_cast(parentNode.Handle) + 1; + Add(state, "..", parentNumber, GuestDirectoryType); + if (hasHostDirectory) { + std::error_code error; +#ifdef _WIN32 + const int savedErrno = errno; + const bool followLinks = InsideApp0(directoryPath); + errno = savedErrno; +#endif + for (std::filesystem::directory_iterator it(directoryPath, error), end; it != end; it.increment(error)) { + if (error) throw std::system_error(error, "read directory descriptor"); + const auto utf8Name = it->path().filename().u8string(); + const std::string name(reinterpret_cast(utf8Name.data()), utf8Name.size()); + FileStat entryStatus{}; + if (!File::FillLinkStat(it->path(), &entryStatus)) throw std::system_error(errno, std::generic_category(), "stat directory entry"); +#ifdef _WIN32 + Add(state, name, static_cast(entryStatus.st_ino), EntryType(it->path(), followLinks)); +#else + Add(state, name, static_cast(entryStatus.st_ino), DirectoryEntryType(it->symlink_status(error))); +#endif + if (error) throw std::system_error(error, "stat directory entry"); + } + if (error) throw std::system_error(error, "read directory descriptor"); + } + if (hasArchiveDirectory) { + std::vector hostNames; + hostNames.reserve(state.entries.size()); + for (const auto& entry : state.entries) hostNames.push_back(entry.name); + for (const auto& entry : AppArchiveListUnshadowedDirectory_nid_no_patch(directoryPath, hostNames)) { + Add(state, entry.Name, static_cast(entry.Handle) + 1, entry.Directory ? GuestDirectoryType : GuestRegularType); + } + } + state.loaded = true; +} + +#ifdef _WIN32 +int OpenPlaceholder(const std::filesystem::path& path) { + std::error_code error; + if (std::filesystem::is_directory(path, 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, + &security, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) { + errno = WindowsOpenError_nid_no_patch(GetLastError()); + return -1; + } + const int fd = ::_open_osfhandle(reinterpret_cast(handle), _O_RDONLY | _O_BINARY); + if (fd < 0) CloseHandle(handle); + return fd; + } + return ::_open("NUL", _O_RDONLY | _O_BINARY); +} +#else +int OpenPlaceholder(const std::filesystem::path& path) { + if (AppArchiveHasDirectory_nid_no_patch(path)) return ::open("/dev/null", O_RDONLY | O_CLOEXEC); + return ::open(path.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC); +} +#endif + +#ifdef _WIN32 int OpenStreamDirectory(const wchar_t* path) { return File::OpenDirectoryDescriptor(path); } @@ -76,76 +243,116 @@ struct StreamRegistration { }; const StreamRegistration g_streamRegistration; +#endif } namespace File { int OpenDirectoryDescriptor(const std::filesystem::path& path) { - const auto state = std::make_shared(); - 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, - &security, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); - if (handle == INVALID_HANDLE_VALUE) { - errno = WindowsOpenError_nid_no_patch(GetLastError()); - return -1; - } - const int fd = ::_open_osfhandle(reinterpret_cast(handle), _O_RDONLY | _O_BINARY); - if (fd < 0) { - CloseHandle(handle); - return -1; - } + auto state = std::make_shared(); + state->path = path; + std::error_code statusError; + const bool hostDirectory = std::filesystem::is_directory(path, statusError); + AppArchiveNode archiveNode{}; + const bool archiveDirectory = AppArchiveLookup_nid_no_patch(path, &archiveNode) && archiveNode.Directory; + state->virtualNamespace = archiveDirectory; +#ifdef _WIN32 + state->nativeBacked = hostDirectory; +#else + state->nativeBacked = hostDirectory && !AppArchiveHasDirectory_nid_no_patch(path); +#endif + const int fd = OpenPlaceholder(path); + if (fd < 0) return -1; try { std::lock_guard lock(g_mutex); g_directories[fd] = state; - g_directoryCount = g_directories.size(); + g_directoryCount.store(g_directories.size(), std::memory_order_relaxed); } catch (...) { +#ifdef _WIN32 ::_close(fd); +#else + ::close(fd); +#endif throw; } return fd; } bool IsDirectoryDescriptor(int fd) { - return g_directoryCount != 0 && Find(fd) != nullptr; + return Find(fd) != nullptr; +} + +bool IsVirtualDirectoryDescriptor(int fd) { + const auto state = Find(fd); + return state && state->virtualNamespace; +} + +bool IsNativeDirectoryDescriptor(int fd) { + const auto state = Find(fd); + return state && state->nativeBacked; } std::optional DirectoryDescriptorPath(int fd) { - if (!IsDirectoryDescriptor(fd)) return std::nullopt; - return WindowsDescriptorPath_nid_no_patch(fd); + if (const auto state = Find(fd)) { + if (!state->nativeBacked) return state->path; +#ifdef _WIN32 + return WindowsDescriptorPath_nid_no_patch(fd); +#else + if (fd < 0) return std::nullopt; + std::error_code error; + const auto link = std::filesystem::path("/proc/self/fd") / std::to_string(fd); + const auto target = std::filesystem::read_symlink(link, error); + if (error) return std::nullopt; + return target; +#endif + } +#ifdef _WIN32 + return std::nullopt; +#else + if (fd < 0) return std::nullopt; + std::error_code error; + const auto link = std::filesystem::path("/proc/self/fd") / std::to_string(fd); + const auto target = std::filesystem::read_symlink(link, error); + if (error) return std::nullopt; + if (!std::filesystem::is_directory(target, error)) return std::nullopt; + return target; +#endif } void ForgetDirectoryDescriptor(int fd) { - if (g_directoryCount == 0) return; + if (g_directoryCount.load(std::memory_order_relaxed) == 0) return; std::lock_guard lock(g_mutex); g_directories.erase(fd); - g_directoryCount = g_directories.size(); + g_directoryCount.store(g_directories.size(), std::memory_order_relaxed); } void DuplicateDirectoryDescriptor(int from, int to) { - if (g_directoryCount == 0) return; + if (g_directoryCount.load(std::memory_order_relaxed) == 0) return; std::lock_guard lock(g_mutex); const auto found = g_directories.find(from); - if (found == g_directories.end()) return; - g_directories[to] = found->second; - g_directoryCount = g_directories.size(); + if (found == g_directories.end()) g_directories.erase(to); + else g_directories[to] = found->second; + g_directoryCount.store(g_directories.size(), std::memory_order_relaxed); } std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, int whence) { + const auto state = Find(fd); + if (!state) return std::nullopt; + std::lock_guard lock(state->mutex); constexpr int SeekSet = 0; constexpr int SeekCurrent = 1; constexpr int SeekEnd = 2; - if (g_directoryCount == 0) return std::nullopt; - const auto found = Find(fd); - if (!found) return std::nullopt; - auto& state = *found; - std::lock_guard lock(state.mutex); if (whence != SeekSet && whence != SeekCurrent && whence != SeekEnd) { errno = EINVAL; return std::int64_t{-1}; } - if (whence == SeekEnd) Load(state, fd); - const auto cursor = static_cast(whence == SeekEnd ? state.entries.size() : state.cursor); + if (whence == SeekEnd) { + try { Load(*state, fd); } + catch (const std::system_error& error) { errno = DirectoryError(error.code()); return std::int64_t{-1}; } + catch (const std::exception&) { errno = EIO; return std::int64_t{-1}; } + } + const auto cursor = static_cast(whence == SeekEnd ? state->entries.size() : state->cursor); if (whence == SeekCurrent && offset == 0) return cursor; if ((whence == SeekCurrent || whence == SeekEnd) && offset > std::numeric_limits::max() - cursor) { errno = EOVERFLOW; @@ -157,69 +364,55 @@ std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, return std::int64_t{-1}; } if (target == 0) { - state.entries.clear(); - state.loaded = false; - state.cursor = 0; + state->entries.clear(); + state->loaded = false; + state->cursor = 0; return std::int64_t{0}; } - Load(state, fd); - state.cursor = static_cast(target); + try { Load(*state, fd); } + catch (const std::system_error& error) { errno = DirectoryError(error.code()); return std::int64_t{-1}; } + state->cursor = static_cast(target); return target; } int ReadDirectoryDescriptor(int fd, char* buf, int nbytes, std::int64_t* base) { - const auto found = Find(fd); - if (!found) return SCE_KERNEL_ERROR_ENOTDIR; - auto& state = *found; - std::lock_guard lock(state.mutex); - Load(state, fd); - if (base != nullptr) *base = static_cast(state.cursor); + const auto state = Find(fd); + if (!state) return SCE_KERNEL_ERROR_ENOTDIR; + if (buf == nullptr) return SCE_KERNEL_ERROR_EFAULT; + if (nbytes <= 0) return SCE_KERNEL_ERROR_EINVAL; + std::lock_guard lock(state->mutex); + try { Load(*state, fd); } + catch (const std::system_error& error) { + return SceKernelError(DirectoryError(error.code())); + } + catch (const DirectoryEntryNameTooLong&) { throw; } + catch (const std::exception&) { return SCE_KERNEL_ERROR_EIO; } + if (base != nullptr) *base = static_cast(state->cursor); std::size_t used = 0; - while (state.cursor < state.entries.size()) { - const auto& [name, type] = state.entries[state.cursor]; - if (name.size() > 255) { - throw std::runtime_error("Directory entry name exceeds the guest 255-byte limit"); - } - const std::size_t record = (8 + name.size() + 1 + 3) & ~std::size_t{3}; + while (state->cursor < state->entries.size()) { + const auto& entry = state->entries[state->cursor]; + const std::size_t record = (8 + entry.name.size() + 1 + 3) & ~std::size_t{3}; if (used + record > static_cast(nbytes)) { if (used == 0) return SCE_KERNEL_ERROR_EINVAL; break; } char* out = buf + used; std::memset(out, 0, record); - const auto fileNumber = static_cast(state.cursor + 1); const auto recordLength = static_cast(record); - std::memcpy(out, &fileNumber, sizeof(fileNumber)); + const auto nameLength = static_cast(entry.name.size()); + std::memcpy(out, &entry.fileNumber, sizeof(entry.fileNumber)); std::memcpy(out + 4, &recordLength, sizeof(recordLength)); - out[6] = static_cast(type); - out[7] = static_cast(name.size()); - std::memcpy(out + 8, name.c_str(), name.size() + 1); + out[6] = static_cast(entry.type); + out[7] = static_cast(nameLength); + std::memcpy(out + 8, entry.name.c_str(), entry.name.size() + 1); used += record; - ++state.cursor; + ++state->cursor; } return static_cast(used); } } -#else - -namespace File { - -std::optional DirectoryDescriptorPath(int fd) { - if (fd < 0) return std::nullopt; - std::error_code error; - const auto link = std::filesystem::path("/proc/self/fd") / std::to_string(fd); - const auto target = std::filesystem::read_symlink(link, error); - if (error) return std::nullopt; - if (!std::filesystem::is_directory(target, error)) return std::nullopt; - return target; -} - -} - -#endif - extern "C" const char* GetDirectoryDescriptorPath_nid_no_patch(int fd, int* error) { if (error == nullptr) return nullptr; if (!DescriptorIsOpen_nid_no_patch(fd)) { @@ -244,11 +437,28 @@ extern "C" const char* GetDirectoryDescriptorPath_nid_no_patch(int fd, int* erro } extern "C" std::uint64_t GetDirectoryDescriptorPosition_nid_no_patch(int fd) { -#ifdef _WIN32 const auto position = File::SeekDirectoryDescriptor(fd, 0, 1); return position && *position > 0 ? static_cast(*position) : 0; -#else - (void)fd; - return 0; -#endif +} + +extern "C" int IsDirectoryDescriptor_nid_no_patch(int fd) { + return File::IsDirectoryDescriptor(fd) ? 1 : 0; +} + +extern "C" int ReadDirectoryDescriptor_nid_no_patch(int fd, char* buf, int nbytes) { + return File::ReadDirectoryDescriptor(fd, buf, nbytes); +} + +extern "C" std::int64_t SeekDirectoryDescriptor_nid_no_patch(int fd, std::int64_t offset, int whence) { + const auto position = File::SeekDirectoryDescriptor(fd, offset, whence); + return position ? *position : -1; +} + +extern "C" int APS5_VABI ReadDirectoryDescriptorCallback_nid_no_patch(int fd, char* buf, int nbytes) { + return File::ReadDirectoryDescriptor(fd, buf, nbytes); +} + +extern "C" std::int64_t APS5_VABI SeekDirectoryDescriptorCallback_nid_no_patch(int fd, std::int64_t offset, int whence) { + const auto position = File::SeekDirectoryDescriptor(fd, offset, whence); + return position ? *position : -1; } diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index 1fda5aabf7..2902da1152 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libkernel/File/include/ArchivedFile.hpp" +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libc/include/General.hpp" #include @@ -171,6 +172,14 @@ void FillFileStat(int nativeDescriptor, FileStat* sb) { bool FillFileStatFromDescriptor(int fd, FileStat* sb) { if (FillArchivedStatFromDescriptor(fd, sb)) return true; + if (File::IsDirectoryDescriptor(fd) && !File::IsNativeDirectoryDescriptor(fd)) { + const auto directory = DirectoryDescriptorPath(fd); + if (!directory) { errno = ENOENT; return false; } + if (FillArchivedStat(*directory, sb)) return true; + if (TryFillFileStat(*directory, sb)) return true; + errno = ENOENT; + return false; + } NativeStat st{}; if (DoFstat(fd, &st) != 0) return false; CopyNativeStat(st, sb); diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 49bf78e813..7609a30cf4 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -6,6 +6,7 @@ #include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/RenameDot.hpp" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/AppArchive.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/SharedMemory.hpp" @@ -201,18 +202,34 @@ static int SceIoErrorFromErrno(int error) { } static int OpenNative(const std::filesystem::path& native, int flags, std::uint16_t mode) { + if (AppArchiveHasDirectory_nid_no_patch(native)) { + std::error_code error; + const auto status = std::filesystem::status(native, error); + const bool hostDirectory = !error && std::filesystem::is_directory(status); + if (hostDirectory || AppArchiveOnly_nid_no_patch(native)) { + if ((flags & SCE_KERNEL_O_ACCMODE) != SCE_KERNEL_O_RDONLY || + (flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_TRUNC)) != 0) return SceErrorFromErrno(EISDIR); + const int fd = File::OpenDirectoryDescriptor(native); + if (fd < 0) return SceErrorFromErrno(errno); + File::RememberDescriptor(fd, flags); + return fd; + } + } if (const auto archived = File::OpenArchivedFile(native, flags)) { if (*archived < 0) return SceErrorFromErrno(-*archived); File::RememberDescriptor(*archived, flags); return *archived; } - File::RefuseMergedDirectory(native); #ifdef _WIN32 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); 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 (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) return SceErrorFromErrno(EEXIST); + } #endif if ((flags & SCE_KERNEL_O_NOFOLLOW) != 0 && (flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) != (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) { constexpr int GuestEmlink = 31; @@ -262,7 +279,10 @@ extern "C" { int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { if (File::InvalidOpenFlags(flags)) return SCE_KERNEL_ERROR_EINVAL; - if (path != nullptr && *path == '\0') return SCE_KERNEL_ERROR_ENOENT; + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceKernelError(error); + path = copy.c_str(); + if (copy.empty()) return SCE_KERNEL_ERROR_ENOENT; APS5_LOG_OUT("path=%s flags=0x%X nativeFlags=0x%X mode=0%o", path, flags, MapFlags(flags), mode); if (path != nullptr && File::IsRandomDevicePath(path)) { if ((flags & SCE_KERNEL_O_ACCMODE) != SCE_KERNEL_O_RDONLY) throw std::runtime_error(std::string(__func__) + ": writing to " + path + " is not implemented"); @@ -305,9 +325,9 @@ int APS5_VABI sceKernelClose(int d) { File::ForgetDescriptor(d); ForgetJitSharedMemory(d); #ifdef _WIN32 - File::ForgetDirectoryDescriptor(d); File::ForgetFileLock(d); #endif + File::ForgetDirectoryDescriptor(d); if (NativeClose(d) != 0) { if (errno == EBADF) return SCE_KERNEL_ERROR_EBADF; throw std::runtime_error(std::string(__func__) + ": close failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); @@ -316,6 +336,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 (GuestSockets::IsOpen(d)) { const auto n = GuestSockets::Read(d, buf, nbytes); return n < 0 ? SceKernelError(*__error_nid_postfix()) : n; @@ -335,6 +356,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 (GuestSockets::IsOpen(d)) { const auto n = GuestSockets::Write(d, buf, nbytes); return n < 0 ? SceKernelError(*__error_nid_postfix()) : n; @@ -344,6 +366,7 @@ std::int64_t APS5_VABI sceKernelWrite(int d, const void* buf, std::size_t nbytes const char empty = 0; return sceKernelWrite(d, &empty, 0); } + const File::WriteSync sync(d); GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes); auto n = NativeWrite(d, buf, nbytes); if (n < 0) return SceIoErrorFromErrno(errno); @@ -368,12 +391,10 @@ std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence) { throw std::invalid_argument(std::string(__func__) + ": invalid whence=" + std::to_string(whence)); } if (const auto archived = File::SeekArchivedFile(d, offset, whence)) return *archived < 0 ? SceIoErrorFromErrno(-*archived) : *archived; -#ifdef _WIN32 if (const auto directory = File::SeekDirectoryDescriptor(d, offset, whence)) { if (*directory < 0) return SceIoErrorFromErrno(errno); return *directory; } -#endif std::int64_t result = NativeLseek(d, offset, whence); if (result < 0) { const int error = errno; @@ -411,13 +432,17 @@ static int StatNative(const std::filesystem::path& native, bool directory, FileS } if (directory && !std::filesystem::is_directory(status)) return SceErrorFromErrno(GuestEnotdir); if (sb == nullptr) return SceErrorFromErrno(GuestEfault); - if (!File::TryFillFileStat(metadataPath, sb)) return SceStatErrorFromErrno(errno); + FileStat result{}; + if (!File::TryFillFileStat(metadataPath, &result)) return SceStatErrorFromErrno(errno); + if (!GuestCopyOut_nid_no_patch(sb, &result, sizeof(result))) return SceErrorFromErrno(GuestEfault); return 0; } int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { - if (path == nullptr) return SceErrorFromErrno(GuestEfault); - if (*path == '\0') return SceErrorFromErrno(2); + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceErrorFromErrno(error); + path = copy.c_str(); + if (copy.empty()) return SceErrorFromErrno(2); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native, true, true)) return SceKernelError(error); return StatNative(native, std::string_view(path).back() == '/', sb); @@ -435,8 +460,10 @@ int StatInDirectory_nid_no_patch(int directory, const char* path, FileStat* sb, } int APS5_VABI sceKernelUnlink(const char* path) { - if (path == nullptr) return SceErrorFromErrno(GuestEfault); - if (*path == '\0') return SceErrorFromErrno(2); + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceErrorFromErrno(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); diff --git a/core/libs/prx/libkernel/File/src/SharedMemory.cpp b/core/libs/prx/libkernel/File/src/SharedMemory.cpp index 67315a6478..8e18c4bcb6 100644 --- a/core/libs/prx/libkernel/File/src/SharedMemory.cpp +++ b/core/libs/prx/libkernel/File/src/SharedMemory.cpp @@ -992,6 +992,9 @@ extern "C" int APS5_VABI shm_open_nid_postfix(const char* path, int flags, int m if (accessMode != SCE_KERNEL_O_RDONLY && accessMode != SCE_KERNEL_O_RDWR) return Fail(GuestInvalid); if ((flags & ~AcceptedFlags) != 0) return Fail(GuestInvalid); if (path != anonymousObject) { + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return Fail(error); + path = copy.c_str(); #ifdef _WIN32 return OpenNamedFile(path, flags, mode); #else @@ -1007,6 +1010,9 @@ extern "C" int APS5_VABI shm_open_nid_postfix(const char* path, int flags, int m } extern "C" int APS5_VABI shm_unlink_nid_postfix(const char* path) { + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return Fail(error); + path = copy.c_str(); #ifdef _WIN32 return UnlinkNamedFile(path); #else diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 338dc6c3a8..9068334dea 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -89,16 +89,7 @@ static int NativeMkdir(const std::filesystem::path& path, std::uint16_t mode) { (void)mode; return ::_wmkdir(path.wstring().c_str()); } -static int NativeChmod(const std::filesystem::path& path, int mode) { - RecordWrittenPath_nid_no_patch(path); - return ::_wchmod(path.wstring().c_str(), mode); -} -std::optional File::WindowsDescriptorPath_nid_no_patch(int descriptor) { - const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); - if (handle == INVALID_HANDLE_VALUE) { - errno = EBADF; - return std::nullopt; - } +static std::optional HandlePath(HANDLE handle) { std::wstring path(MAX_PATH, L'\0'); auto length = ::GetFinalPathNameByHandleW(handle, path.data(), static_cast(path.size()), FILE_NAME_NORMALIZED); if (length >= path.size()) { @@ -118,6 +109,32 @@ std::optional File::WindowsDescriptorPath_nid_no_patch(in else if (path.starts_with(LR"(\\?\)") && path.size() > 6 && path[5] == L':' && path[6] == L'\\') path.erase(0, 4); return path; } +std::optional File::WindowsDescriptorPath_nid_no_patch(int descriptor) { + const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + if (handle == INVALID_HANDLE_VALUE) { + errno = EBADF; + return std::nullopt; + } + return HandlePath(handle); +} +static int NativeChmod(const std::filesystem::path& path, int mode) { + const DWORD attributes = GetFileAttributesW(path.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); + } + const HANDLE handle = CreateFileW(path.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()); + return -1; + } + const auto target = HandlePath(handle); + CloseHandle(handle); + if (!target) return -1; + RecordWrittenPath_nid_no_patch(*target); + return ::_wchmod(target->wstring().c_str(), mode); +} static int NativeFchmod(int descriptor, int mode) { const auto path = File::WindowsDescriptorPath_nid_no_patch(descriptor); return path ? NativeChmod(*path, mode) : -1; @@ -187,6 +204,7 @@ static int NativeUtimes(const std::filesystem::path& path, const KernelTimeval* } static int NativeFutimes(int descriptor, const KernelTimeval* times) { if (const auto directory = File::DirectoryDescriptorPath(descriptor)) return NativeUtimes(*directory, times); + const auto original = reinterpret_cast(::_get_osfhandle(descriptor)); if (original == INVALID_HANDLE_VALUE) { errno = EBADF; return -1; } if (GetFileType(original) != FILE_TYPE_DISK) { errno = EINVAL; return -1; } @@ -445,10 +463,17 @@ static bool IsPipeDescriptor(int descriptor) { #endif } -static int PositionedArgumentError(int descriptor, bool nullBuffer, std::int64_t offset) { +static bool IsSeekableDescriptor(int descriptor) { + if (GuestSockets::IsOpen(descriptor)) return false; + return File::IsVnode(descriptor) || File::IsCharacterDevice(descriptor) || File::IsRandomDevice(descriptor); +} + +static int PositionedArgumentError(int descriptor, bool write, bool nullBuffer, std::int64_t offset) { + if (IsKqueueDescriptor(descriptor)) return GUEST_ESPIPE; if (!nullBuffer && offset >= 0) return 0; if (!GuestSockets::IsOpen(descriptor) && !DescriptorIsOpen_nid_no_patch(descriptor)) return GUEST_EBADF; - if (GuestSockets::IsOpen(descriptor) || IsPipeDescriptor(descriptor)) return GUEST_ESPIPE; + if (!IsSeekableDescriptor(descriptor)) return GUEST_ESPIPE; + if (!File::GrantsAccess(descriptor, write)) return GUEST_EBADF; return offset < 0 ? GUEST_EINVAL : GUEST_EFAULT; } @@ -597,9 +622,9 @@ extern "C" int APS5_VABI pipe_nid_postfix(int* descriptors) { return 0; } -static int PathError(const char* path) { - if (path == nullptr) return GUEST_EFAULT; - return *path == '\0' ? GUEST_ENOENT : 0; +static int PathError(const char* path, std::string& copy) { + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return error; + return copy.empty() ? GUEST_ENOENT : 0; } #ifdef _WIN32 @@ -627,6 +652,8 @@ static bool HasNonDirectoryPrefix(const std::filesystem::path& native) { int File::DuplicateFrom(int fd, int start) { #ifdef _WIN32 + GuestResourceLimit openFileLimit{}; + if (getrlimit_nid_postfix(8, &openFileLimit) == 0 && start >= openFileLimit.rlim_cur) return PosixFailure(GUEST_EINVAL); std::vector below; int duplicate; while ((duplicate = dup_nid_postfix(fd)) >= 0 && duplicate < start) below.push_back(duplicate); @@ -636,6 +663,7 @@ int File::DuplicateFrom(int fd, int start) { #else const int duplicate = ::fcntl(fd, F_DUPFD, start); if (duplicate < 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); + File::DuplicateDirectoryDescriptor(fd, duplicate); if (File::IsRandomDevice(fd)) File::RememberRandomDevice(duplicate); File::ShareDescriptor(fd, duplicate); return duplicate; @@ -643,6 +671,15 @@ 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); + 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; + } +#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; @@ -682,7 +719,9 @@ static int SetFileFlags(const char* function, const std::string& target, std::ui extern "C" { int APS5_VABI chmod_nid_postfix(const char* path, int mode) { - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); if (NativeChmod(native, mode & 07777) != 0) return PosixResult(SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno)); @@ -690,7 +729,9 @@ int APS5_VABI chmod_nid_postfix(const char* path, int mode) { } int APS5_VABI chflags_nid_postfix(const char* path, std::uint64_t flags) { - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); if (const int error = LookupError(native)) return PosixFailure(error); @@ -699,6 +740,7 @@ int APS5_VABI chflags_nid_postfix(const char* path, std::uint64_t flags) { } int APS5_VABI fchflags_nid_postfix(int d, std::uint64_t flags) { + if (File::IsVirtualDirectoryDescriptor(d)) return PosixFailure(GUEST_EISDIR); if (GuestSockets::IsOpen(d) || IsKqueueDescriptor(d) || File::IsSharedMemoryDescriptor_nid_no_patch(d)) return PosixFailure(GUEST_EINVAL); File::RefuseArchivedFile(d, "fchflags"); if (!File::IsVnode(d)) { @@ -734,8 +776,8 @@ int APS5_VABI close_nid_postfix(int d) { File::ForgetRandomDevice(d); File::ForgetDescriptor(d); ForgetJitSharedMemory(d); -#ifdef _WIN32 File::ForgetDirectoryDescriptor(d); +#ifdef _WIN32 File::ForgetFileLock(d); const int shared = File::CloseSharedMemoryDescriptor_nid_no_patch(d); if (shared != -2) return shared; @@ -778,12 +820,12 @@ int APS5_VABI dup_nid_postfix(int d) { if (IsKqueueDescriptor(d)) throw std::runtime_error(std::string(__func__) + ": duplicating a kqueue descriptor is not supported"); #ifdef _WIN32 const int duplicate = WithoutParameterHandler([d] { return ::_dup(d); }); - if (duplicate >= 0) File::DuplicateDirectoryDescriptor(d, duplicate); if (duplicate >= 0) File::DuplicateSharedMemoryDescriptor_nid_no_patch(d, duplicate); #else const int duplicate = ::dup(d); #endif if (duplicate < 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); + File::DuplicateDirectoryDescriptor(d, duplicate); if (File::IsRandomDevice(d)) File::RememberRandomDevice(duplicate); File::ShareDescriptor(d, duplicate); DuplicateJitSharedMemory(d, duplicate); @@ -799,8 +841,8 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { if (from == to) return to; File::ForgetRandomDevice(to); File::ForgetArchivedFile(to); -#ifdef _WIN32 File::ForgetDirectoryDescriptor(to); +#ifdef _WIN32 File::ForgetFileLock(to); #endif return to; @@ -840,6 +882,8 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { if (to == 2) PreserveHostStandardError(); const int result = ::dup2(from, to); if (result < 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); + File::ForgetDirectoryDescriptor(to); + File::DuplicateDirectoryDescriptor(from, to); EqueueDescriptorClosed(to); GuestSockets::Forget(to); File::ForgetRandomDevice(to); @@ -851,6 +895,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); File::RefuseArchivedFile(d, "flock"); #ifdef _WIN32 @@ -882,16 +927,14 @@ int APS5_VABI flock_nid_postfix(int d, int operation) { } int APS5_VABI _fstatfs_nid_postfix(int d, KernelStatfs* buf) { + if (File::IsVirtualDirectoryDescriptor(d)) return PosixFailure(GUEST_EOPNOTSUPP); if (GuestSockets::IsOpen(d) || IsKqueueDescriptor(d)) return PosixFailure(GUEST_EINVAL); if (d < 0) return PosixFailure(GUEST_EBADF); File::RefuseArchivedFile(d, "fstatfs"); KernelStatfs result{}; result.f_version = KERNEL_STATFS_VERSION; if (const int error = NativeFstatfs(d, &result)) return PosixFailure(error); - if (buf == nullptr) return PosixFailure(GUEST_EFAULT); - const GuestArena::HostWrite destination(buf, sizeof(result)); - if (!destination.Open()) return PosixFailure(GUEST_EFAULT); - std::memcpy(buf, &result, sizeof(result)); + if (!GuestCopyOut_nid_no_patch(buf, &result, sizeof(result))) return PosixFailure(GUEST_EFAULT); return 0; } @@ -901,11 +944,13 @@ int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int d, FileStat* sb) { if (fstat_nid_disambig1_nid_postfix(d, &ignored) != 0) return -1; return PosixFailure(GUEST_EFAULT); } + FileStat result{}; if (GuestSockets::IsOpen(d)) { - if (const int error = GuestSockets::Stat(d, sb)) return PosixFailure(error); - return 0; + if (const int error = GuestSockets::Stat(d, &result)) return PosixFailure(error); + } else if (!File::FillFileStatFromDescriptor(d, &result)) { + return PosixFailure(SceErrorFromErrno(errno) & 0xffff); } - if (!File::FillFileStatFromDescriptor(d, sb)) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); + if (!GuestCopyOut_nid_no_patch(sb, &result, sizeof(result))) return PosixFailure(GUEST_EFAULT); return 0; } @@ -914,6 +959,7 @@ int64_t APS5_VABI _fstat_nid_postfix(int d, FileStat* sb) { } int APS5_VABI ftruncate_nid_postfix(int d, int64_t length) { + if (File::IsDirectoryDescriptor(d)) return PosixFailure(GUEST_EINVAL); if (length < 0 || GuestSockets::IsOpen(d)) { return PosixFailure(GUEST_EINVAL); } @@ -943,7 +989,9 @@ int64_t APS5_VABI lseek_nid_postfix(int d, int64_t offset, int whence) { } int APS5_VABI mkdir_nid_postfix(const char* path, uint16_t mode) { - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); if (NativeMkdir(native, mode) != 0) { @@ -960,7 +1008,9 @@ int APS5_VABI mkdir_nid_postfix(const char* path, uint16_t mode) { int APS5_VABI open_nid_postfix(const char* path, int flags, int mode) { if (File::InvalidOpenFlags(flags)) return PosixFailure(GUEST_EINVAL); - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); return PosixResult(sceKernelOpen(path, flags, static_cast(mode))); } @@ -987,8 +1037,9 @@ int APS5_VABI _open_nid_postfix(const char* path, int flags, ...) { } int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t offset) { + if (File::IsDirectoryDescriptor(d)) return PosixFailure(GUEST_EISDIR); if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); - if (const int error = PositionedArgumentError(d, buf == nullptr && nbytes != 0, offset)) return PosixFailure(error); + if (const int error = PositionedArgumentError(d, false, buf == nullptr && nbytes != 0, offset)) return PosixFailure(error); if (FailPosixPositionedGuestSocket(d)) return -1; const GuestArena::HostWrite destination(buf, nbytes); if (!destination.Open()) return PosixFailure(GUEST_EFAULT); @@ -1000,9 +1051,11 @@ int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t off } int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int d, const void* buf, size_t nbytes, int64_t offset) { + if (File::IsVirtualDirectoryDescriptor(d)) return PosixFailure(GUEST_EISDIR); if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); - if (const int error = PositionedArgumentError(d, buf == nullptr && nbytes != 0, offset)) return PosixFailure(error); + if (const int error = PositionedArgumentError(d, true, buf == nullptr && nbytes != 0, offset)) return PosixFailure(error); if (FailPosixPositionedGuestSocket(d)) return -1; + const File::WriteSync sync(d); GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes); char empty = 0; const auto result = NativePwrite(d, buf == nullptr ? &empty : buf, nbytes, offset); @@ -1043,27 +1096,38 @@ std::int64_t APS5_VABI _write_nid_postfix(int descriptor, const void* buffer, st int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb) { if (sb == nullptr) return PosixFailure(GUEST_EFAULT); - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); return PosixResult(sceKernelStat(path, sb)); } int APS5_VABI lstat_nid_postfix(const char* path, FileStat* sb) { if (sb == nullptr) return PosixFailure(GUEST_EFAULT); - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); std::filesystem::path native; - if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); - if (!File::FillLinkStat(native, sb)) return PosixResult(SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno)); + if (const int error = ResolvePathError_nid_no_patch(path, native, true)) return PosixFailure(error); + FileStat result{}; + if (!File::FillLinkStat(native, &result)) return PosixResult(SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno)); + if (!GuestCopyOut_nid_no_patch(sb, &result, sizeof(result))) return PosixFailure(GUEST_EFAULT); return 0; } int APS5_VABI unlink_nid_postfix(const char* path) { - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); return PosixResult(sceKernelUnlink(path)); } int APS5_VABI sceKernelCheckReachability(const char* path) { if (path == nullptr) return SCE_KERNEL_ERROR_EINVAL; - if (*path == '\0') return SCE_KERNEL_ERROR_ENOENT; + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceKernelError(error); + path = copy.c_str(); + if (copy.empty()) return SCE_KERNEL_ERROR_ENOENT; if (std::strlen(path) > 255) return SCE_KERNEL_ERROR_ENAMETOOLONG; std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native, true, true)) return SceKernelError(error); @@ -1078,15 +1142,19 @@ int APS5_VABI sceKernelFstat(int d, FileStat* sb) { if (const int error = sceKernelFstat(d, &ignored)) return error; return SceErrorFromErrno(GUEST_EFAULT); } + FileStat result{}; if (GuestSockets::IsOpen(d)) { - const int error = GuestSockets::Stat(d, sb); - return error == 0 ? 0 : SceErrorFromErrno(error); + const int error = GuestSockets::Stat(d, &result); + if (error != 0) return SceErrorFromErrno(error); + } else if (!File::FillFileStatFromDescriptor(d, &result)) { + return SceErrorFromErrno(errno); } - if (!File::FillFileStatFromDescriptor(d, sb)) return SceErrorFromErrno(errno); + if (!GuestCopyOut_nid_no_patch(sb, &result, sizeof(result))) return SceErrorFromErrno(GUEST_EFAULT); return 0; } int APS5_VABI sceKernelFsync(int fd) { + if (File::IsVirtualDirectoryDescriptor(fd)) return SceErrorFromErrno(GUEST_EISDIR); if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); File::RefuseArchivedFile(fd, "fsync"); #ifdef _WIN32 @@ -1098,6 +1166,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); File::RefuseArchivedFile(fd, "fdatasync"); #ifdef _WIN32 @@ -1176,12 +1245,15 @@ int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* bas constexpr std::size_t GuestMaxName = 255; if (buf == nullptr) return SceErrorFromErrno(GUEST_EFAULT); if (nbytes <= 0) return SceErrorFromErrno(GUEST_EINVAL); - struct stat status{}; - if (::fstat(fd, &status) != 0) return SceErrorFromErrno(errno); - if (!S_ISDIR(status.st_mode)) return SceErrorFromErrno(GUEST_EINVAL); const GuestArena::HostWrite destination(buf, static_cast(nbytes)); const GuestArena::HostWrite baseDestination(basep, basep == nullptr ? 0 : sizeof(*basep)); if (!destination.Open() || !baseDestination.Open()) return SceErrorFromErrno(GUEST_EFAULT); + if (File::IsDirectoryDescriptor(fd)) { + return File::ReadDirectoryDescriptor(fd, buf, nbytes, basep); + } + struct stat status{}; + if (::fstat(fd, &status) != 0) return SceErrorFromErrno(errno); + if (!S_ISDIR(status.st_mode)) return SceErrorFromErrno(GUEST_EINVAL); const off_t base = ::lseek(fd, 0, SEEK_CUR); if (base < 0) return SceErrorFromErrno(errno); if (basep != nullptr) *basep = static_cast(base); @@ -1238,8 +1310,10 @@ int APS5_VABI getdents_nid_postfix(int fd, char* buf, int nbytes) { int APS5_VABI sceKernelMkdir(const char* path, uint16_t mode) { (void)mode; - if (!path) return SceErrorFromErrno(GUEST_EFAULT); - if (!*path) return SceErrorFromErrno(GUEST_ENOENT); + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceErrorFromErrno(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(); @@ -1281,6 +1355,11 @@ static int CopyIovecs(const KernelIovec* iov, int iovcnt, std::vector static_cast(std::numeric_limits::max()) - total) return SceErrorFromErrno(GUEST_EINVAL); + total += vector.length; + } return 0; } @@ -1307,7 +1386,8 @@ static void PrepareWriteIovecs(const KernelIovec* iov, int iovcnt) { #ifdef _WIN32 int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset) { - if (const int error = PositionedArgumentError(d, buf == nullptr && nbytes != 0, offset)) return SceErrorFromErrno(error); + if (File::IsDirectoryDescriptor(d)) return SceErrorFromErrno(GUEST_EISDIR); + if (const int error = PositionedArgumentError(d, false, buf == nullptr && nbytes != 0, offset)) return SceErrorFromErrno(error); if (const auto error = PositionedGuestSocketError(d); error != 0) return error; const GuestArena::HostWrite destination(buf, nbytes); if (!destination.Open()) return SceErrorFromErrno(GUEST_EFAULT); @@ -1319,8 +1399,10 @@ int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset } int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t offset) { - if (const int error = PositionedArgumentError(d, buf == nullptr && nbytes != 0, offset)) return SceErrorFromErrno(error); + if (File::IsVirtualDirectoryDescriptor(d)) return SceErrorFromErrno(GUEST_EISDIR); + if (const int error = PositionedArgumentError(d, true, buf == nullptr && nbytes != 0, offset)) return SceErrorFromErrno(error); if (const auto error = PositionedGuestSocketError(d); error != 0) return error; + const File::WriteSync sync(d); GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes); char empty = 0; const auto result = NativePwrite(d, nbytes == 0 && buf == nullptr ? &empty : buf, nbytes, offset); @@ -1328,14 +1410,16 @@ int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t } static std::int64_t TransferIovecs(int d, const KernelIovec* guestIov, int iovcnt, const std::int64_t* offset, bool write) { + if (write ? File::IsVirtualDirectoryDescriptor(d) : File::IsDirectoryDescriptor(d)) return SceErrorFromErrno(GUEST_EISDIR); std::vector vectors; if (const int error = CopyIovecs(guestIov, iovcnt, vectors)) return error; const auto* iov = vectors.data(); - if (offset != nullptr && *offset < 0) return SceErrorFromErrno(GUEST_EINVAL); + if (offset != nullptr) { + if (const int error = PositionedArgumentError(d, write, false, *offset)) return SceErrorFromErrno(error); + } std::size_t total = 0; for (int i = 0; i < iovcnt; ++i) { if (iov[i].base == nullptr && iov[i].length != 0) return SceErrorFromErrno(GUEST_EFAULT); - if (iov[i].length > static_cast(std::numeric_limits::max()) - total) return SceErrorFromErrno(GUEST_EINVAL); total += iov[i].length; } if (offset != nullptr && total > static_cast(std::numeric_limits::max() - *offset)) return SceErrorFromErrno(GUEST_EINVAL); @@ -1349,6 +1433,7 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* guestIov, int iovcn : GuestSockets::Readv(d, reinterpret_cast(iov), iovcnt); return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; } + const File::WriteSync sync(write ? d : -1); if (write) PrepareWriteIovecs(iov, iovcnt); if (!write && File::IsRandomDevice(d)) return ReadRandomIovecs(d, iov, iovcnt); if (!write) { @@ -1389,25 +1474,30 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* guestIov, int iovcn } int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* iov, int iovcnt) { + if (File::IsDirectoryDescriptor(d)) return SceErrorFromErrno(GUEST_EISDIR); return TransferIovecs(d, iov, iovcnt, nullptr, false); } int64_t APS5_VABI sceKernelWritev(int d, const KernelIovec* iov, int iovcnt) { + if (File::IsVirtualDirectoryDescriptor(d)) return SceErrorFromErrno(GUEST_EISDIR); return TransferIovecs(d, iov, iovcnt, nullptr, true); } int64_t APS5_VABI sceKernelPreadv(int d, const KernelIovec* iov, int iovcnt, int64_t offset) { + if (File::IsDirectoryDescriptor(d)) return SceErrorFromErrno(GUEST_EISDIR); return TransferIovecs(d, iov, iovcnt, &offset, false); } int64_t APS5_VABI sceKernelPwritev(int d, const KernelIovec* iov, int iovcnt, int64_t offset) { + if (File::IsVirtualDirectoryDescriptor(d)) return SceErrorFromErrno(GUEST_EISDIR); return TransferIovecs(d, iov, iovcnt, &offset, true); } #else int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset) { - if (const int error = PositionedArgumentError(d, buf == nullptr && nbytes != 0, offset)) return SceErrorFromErrno(error); + if (File::IsDirectoryDescriptor(d)) return SceErrorFromErrno(GUEST_EISDIR); + if (const int error = PositionedArgumentError(d, false, buf == nullptr && nbytes != 0, offset)) return SceErrorFromErrno(error); if (const auto error = PositionedGuestSocketError(d); error != 0) return error; const GuestArena::HostWrite destination(buf, nbytes); if (!destination.Open()) return SceErrorFromErrno(GUEST_EFAULT); @@ -1418,14 +1508,17 @@ int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset } int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t offset) { - if (const int error = PositionedArgumentError(d, buf == nullptr && nbytes != 0, offset)) return SceErrorFromErrno(error); + if (File::IsVirtualDirectoryDescriptor(d)) return SceErrorFromErrno(GUEST_EISDIR); + if (const int error = PositionedArgumentError(d, true, buf == nullptr && nbytes != 0, offset)) return SceErrorFromErrno(error); if (const auto error = PositionedGuestSocketError(d); error != 0) return error; + const File::WriteSync sync(d); GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes); const auto result = NativePwrite(d, buf, nbytes, offset); return result < 0 ? SceErrorFromErrno(errno) : result; } int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* guestIov, int iovcnt) { + if (File::IsDirectoryDescriptor(d)) return SceErrorFromErrno(GUEST_EISDIR); std::vector vectors; if (const int error = CopyIovecs(guestIov, iovcnt, vectors)) return error; const auto* iov = vectors.data(); @@ -1442,6 +1535,7 @@ int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* guestIov, int iovcnt) } int64_t APS5_VABI sceKernelWritev(int d, const KernelIovec* guestIov, int iovcnt) { + if (File::IsVirtualDirectoryDescriptor(d)) return SceErrorFromErrno(GUEST_EISDIR); std::vector vectors; if (const int error = CopyIovecs(guestIov, iovcnt, vectors)) return error; const auto* iov = vectors.data(); @@ -1449,6 +1543,7 @@ int64_t APS5_VABI sceKernelWritev(int d, const KernelIovec* guestIov, int iovcnt const auto result = GuestSockets::Writev(d, reinterpret_cast(iov), iovcnt); return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; } + const File::WriteSync sync(d); PrepareWriteIovecs(iov, iovcnt); const auto sharedResult = File::WritevSharedMemory_nid_no_patch(d, NativeIovecs(iov), iovcnt); if (sharedResult != -2) return sharedResult < 0 ? SceErrorFromErrno(errno) : sharedResult; @@ -1457,10 +1552,11 @@ int64_t APS5_VABI sceKernelWritev(int d, const KernelIovec* guestIov, int iovcnt } int64_t APS5_VABI sceKernelPreadv(int d, const KernelIovec* guestIov, int iovcnt, int64_t offset) { + if (File::IsDirectoryDescriptor(d)) return SceErrorFromErrno(GUEST_EISDIR); std::vector vectors; if (const int error = CopyIovecs(guestIov, iovcnt, vectors)) return error; const auto* iov = vectors.data(); - if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); + if (const int error = PositionedArgumentError(d, false, false, offset)) return SceErrorFromErrno(error); if (const auto error = PositionedGuestSocketError(d); error != 0) return error; std::deque destinations; if (!OpenIovecs(iov, iovcnt, destinations)) return SceErrorFromErrno(GUEST_EFAULT); @@ -1471,11 +1567,13 @@ int64_t APS5_VABI sceKernelPreadv(int d, const KernelIovec* guestIov, int iovcnt } int64_t APS5_VABI sceKernelPwritev(int d, const KernelIovec* guestIov, int iovcnt, int64_t offset) { + if (File::IsVirtualDirectoryDescriptor(d)) return SceErrorFromErrno(GUEST_EISDIR); std::vector vectors; if (const int error = CopyIovecs(guestIov, iovcnt, vectors)) return error; const auto* iov = vectors.data(); - if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); + if (const int error = PositionedArgumentError(d, true, false, offset)) return SceErrorFromErrno(error); if (const auto error = PositionedGuestSocketError(d); error != 0) return error; + const File::WriteSync sync(d); PrepareWriteIovecs(iov, iovcnt); const auto sharedResult = File::PwritevSharedMemory_nid_no_patch(d, NativeIovecs(iov), iovcnt, offset); if (sharedResult != -2) return sharedResult < 0 ? SceErrorFromErrno(errno) : sharedResult; @@ -1499,13 +1597,13 @@ int APS5_VABI sceKernelRename(const char* from, const char* to) { if (!from) return SceErrorFromErrno(GUEST_EFAULT); const int result = RenameGuestPath_nid_no_patch(from, to); return result == 0 ? 0 : SceKernelError(errno); - if (const int dot = RenameDotError_nid_no_patch(from)) return SceKernelError(dot); - if (const int dot = RenameDotError_nid_no_patch(to)) return SceKernelError(dot); } int APS5_VABI sceKernelRmdir(const char* path) { - if (!path) return SceErrorFromErrno(GUEST_EFAULT); - if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceErrorFromErrno(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); @@ -1526,7 +1624,9 @@ int APS5_VABI sceKernelRmdir(const char* path) { } int APS5_VABI rmdir_nid_postfix(const char* path) { - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); return PosixResult(sceKernelRmdir(path)); } @@ -1535,8 +1635,10 @@ int APS5_VABI rmdir_nid_postfix(const char* path) { extern "C" { int APS5_VABI sceKernelChmod_nid_postfix(const char* path, std::uint16_t mode) { - if (!path) return SceErrorFromErrno(GUEST_EFAULT); - if (!*path) return SceErrorFromErrno(GUEST_ENOENT); + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceErrorFromErrno(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); if (NativeChmod(native, mode & 07777) != 0) return SceErrorFromErrno(errno); @@ -1544,7 +1646,9 @@ int APS5_VABI sceKernelChmod_nid_postfix(const char* path, std::uint16_t mode) { } int APS5_VABI sceKernelFchmod(int d, std::uint16_t mode) { + if (File::IsVirtualDirectoryDescriptor(d)) return SceErrorFromErrno(GUEST_EISDIR); if (GuestSockets::IsOpen(d)) return SceErrorFromErrno(GUEST_EINVAL); + if (IsPipeDescriptor(d)) return SceErrorFromErrno(GUEST_EINVAL); File::RefuseArchivedFile(d, "fchmod"); #ifdef _WIN32 const int sharedMemoryResult = File::SetSharedMemoryMode_nid_no_patch(d, mode); @@ -1564,8 +1668,10 @@ static bool InvalidTimes(const KernelTimeval* times) { int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t length) { if (length < 0) return SceErrorFromErrno(GUEST_EINVAL); - if (!path) return SceErrorFromErrno(GUEST_EFAULT); - if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceErrorFromErrno(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; @@ -1585,14 +1691,18 @@ int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t lengt int APS5_VABI truncate_nid_postfix(const char* path, std::int64_t length) { if (length < 0) return PosixFailure(GUEST_EINVAL); - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); return PosixResult(sceKernelTruncate_nid_postfix(path, length)); } int APS5_VABI sceKernelUtimes_nid_postfix(const char* path, const KernelTimeval* times) { if (InvalidTimes(times)) return SCE_KERNEL_ERROR_EINVAL; - if (!path) return SceErrorFromErrno(GUEST_EFAULT); - if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceErrorFromErrno(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); if (NativeUtimes(native, times) != 0) return SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno); @@ -1601,13 +1711,17 @@ int APS5_VABI sceKernelUtimes_nid_postfix(const char* path, const KernelTimeval* int APS5_VABI utimes_nid_postfix(const char* path, const KernelTimeval* times) { if (InvalidTimes(times)) return PosixFailure(GUEST_EINVAL); - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); return PosixResult(sceKernelUtimes_nid_postfix(path, times)); } int APS5_VABI lutimes_nid_postfix(const char* path, const KernelTimeval* times) { if (InvalidTimes(times)) return PosixFailure(GUEST_EINVAL); - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); if (NativeUtimes(native, times, false) != 0) return PosixResult(SceErrorFromErrno(errno)); @@ -1615,6 +1729,7 @@ int APS5_VABI lutimes_nid_postfix(const char* path, const KernelTimeval* times) } int APS5_VABI sceKernelFutimes(int d, const KernelTimeval* times) { + if (File::IsVirtualDirectoryDescriptor(d)) return SceErrorFromErrno(GUEST_EISDIR); if (GuestSockets::IsOpen(d)) return SceErrorFromErrno(GUEST_EINVAL); File::RefuseArchivedFile(d, "futimes"); if (times != nullptr) { diff --git a/core/libs/prx/libkernel/KernelErrors.hpp b/core/libs/prx/libkernel/KernelErrors.hpp index b7d37ba4f2..e3e5b759fd 100644 --- a/core/libs/prx/libkernel/KernelErrors.hpp +++ b/core/libs/prx/libkernel/KernelErrors.hpp @@ -99,5 +99,7 @@ inline constexpr int SCE_KERNEL_ERROR_ENOLINK = SceKernelError(91); inline constexpr int SCE_KERNEL_ERROR_EPROTO = SceKernelError(92); inline constexpr int SCE_KERNEL_ERROR_ENOTCAPABLE = SceKernelError(93); inline constexpr int SCE_KERNEL_ERROR_ECAPMODE = SceKernelError(94); +inline constexpr int SCE_KERNEL_ERROR_ENOTRECOVERABLE = SceKernelError(95); +inline constexpr int SCE_KERNEL_ERROR_EOWNERDEAD = SceKernelError(96); #endif diff --git a/core/libs/prx/libkernel/Module/src/Tls.cpp b/core/libs/prx/libkernel/Module/src/Tls.cpp new file mode 100644 index 0000000000..bc3f81240f --- /dev/null +++ b/core/libs/prx/libkernel/Module/src/Tls.cpp @@ -0,0 +1,44 @@ +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/include/Api.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + +namespace { + +#ifdef _WIN32 +constexpr std::size_t ThreadLocalStoragePointerOffset = 0x58; +#endif + +} + +#if defined(__linux__) +extern "C" void* __tls_get_addr(void* index); +#endif + +extern "C" { + +void* APS5_VABI __tls_get_addr_nid_postfix(const KernelTlsIndex* index) { + if (index == nullptr || index->module == 0) throw std::invalid_argument("__tls_get_addr: null TLS index or module"); +#if defined(_WIN32) + const auto slot = *reinterpret_cast(static_cast(index->module)); + auto* blocks = *reinterpret_cast(reinterpret_cast(NtCurrentTeb()) + ThreadLocalStoragePointerOffset); + if (blocks == nullptr || blocks[slot] == nullptr) throw std::logic_error("__tls_get_addr: thread has no TLS block for the module"); + return blocks[slot] + index->offset; +#elif defined(__linux__) + KernelTlsIndex host = *index; + return __tls_get_addr(&host); +#else + NotImplemented_nid_no_patch("__tls_get_addr on this host"); + return nullptr; +#endif +} + +} diff --git a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp index a71ae71597..a9c3f61aed 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp @@ -24,12 +24,16 @@ int APS5_VABI pthread_attr_destroy_nid_postfix(PthreadAttr* attr) { int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr) { if (!thread || !Valid(attr)) return PosixThread::GUEST_EINVAL; + const auto live = LockLiveGuestThread(thread); + if (!live) return PosixThread::GUEST_ESRCH; auto* p = *attr; p->_stacksize = thread->stackSize; p->stackAddress = thread->stackAddress; p->_detachstate = thread->_detached ? 1 : 0; p->_schedpriority = thread->priority.load(std::memory_order_relaxed); p->_schedpolicy = thread->schedPolicy.load(std::memory_order_relaxed); + p->_inheritsched = thread->inheritSched; + p->_createSuspended = thread->createdSuspended; p->_affinity = thread->affinity.load(std::memory_order_relaxed); p->_affinityExplicit = true; p->_scope = thread->scope; diff --git a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp index 43f18c44ce..4c6d721513 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp @@ -1,6 +1,7 @@ #include "prx/libkernel/include/Api.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" #include "prx/libkernel/Pthread/include/Cond.hpp" +#include "prx/libc/include/General.hpp" #include "Common.hpp" #include #include @@ -52,6 +53,10 @@ int APS5_VABI pthread_cond_wait_nid_postfix(PthreadCond* cond, PthreadMutex* mut return toPosix(scePthreadCondWait(cond, mutex)); } +int APS5_VABI pthread_cond_setname_np_nid_postfix(PthreadCond* cond, const char* name) { + return toPosix(CondOperations::SetName(cond, name)); +} + int APS5_VABI pthread_condattr_destroy_nid_postfix(PthreadCondattr* attr) { if (!attr || !*attr) return PosixThread::GUEST_EINVAL; return toPosix(scePthreadCondattrDestroy(attr)); @@ -67,6 +72,12 @@ int APS5_VABI pthread_condattr_setclock_nid_postfix(PthreadCondattr* attr, Kerne return toPosix(scePthreadCondattrSetclock(attr, clock_id)); } +int APS5_VABI pthread_condattr_getclock_nid_postfix(const PthreadCondattr* attr, KernelClockid* clockId) { + if (!attr || !*attr || !clockId) NotImplemented_nid_no_patch(__func__); + *clockId = (*attr)->_clockid; + return 0; +} + int APS5_VABI pthread_condattr_setpshared_nid_postfix(PthreadCondattr* attr, int pshared) { if (!attr || !*attr) return PosixThread::GUEST_EINVAL; if (pshared != POSIX_PROCESS_PRIVATE) return PosixThread::GUEST_EINVAL; diff --git a/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp b/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp index c82db18c88..86039d56d1 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp @@ -14,6 +14,8 @@ namespace { constexpr int GUEST_REALTIME_CLOCK = 0; constexpr int PROCESS_PRIVATE = 0; +constexpr int FIRST_STORED_RWLOCK_TYPE = 1; +constexpr int LAST_STORED_RWLOCK_TYPE = 2; int toPosix(int result) { if (result == 0) @@ -56,6 +58,11 @@ int APS5_VABI pthread_rwlockattr_init_nid_postfix(PthreadRwlockattr* attr) { return toPosix(scePthreadRwlockattrInit(attr)); } +int APS5_VABI pthread_rwlockattr_settype_np_nid_postfix(PthreadRwlockattr* attr, int type) { + if (!attr || !*attr || type < FIRST_STORED_RWLOCK_TYPE || type > LAST_STORED_RWLOCK_TYPE) return PosixThread::GUEST_EINVAL; + return toPosix(scePthreadRwlockattrSettype(attr, type)); +} + int APS5_VABI pthread_rwlockattr_getpshared_nid_postfix(const PthreadRwlockattr* attr, int* pshared) { if (!attr || !*attr) throw std::runtime_error("pthread_rwlockattr_getpshared: null attr"); if (!pshared) throw std::runtime_error("pthread_rwlockattr_getpshared: null pshared"); diff --git a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp index d755e0cf46..fc5ea88197 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp @@ -167,11 +167,9 @@ int APS5_VABI sem_reltimedwait_np_nid_postfix(void* sem, uint32_t usec) { int APS5_VABI sem_post_nid_postfix(void* sem) { auto* semaphore = Get(sem); if (!semaphore) return Fail(PosixThread::GUEST_EINVAL); - { - std::lock_guard lock(semaphore->lock); - if (semaphore->count == GUEST_SEM_VALUE_MAX) return Fail(GUEST_EOVERFLOW); - ++semaphore->count; - } + std::lock_guard lock(semaphore->lock); + if (semaphore->count == GUEST_SEM_VALUE_MAX) return Fail(GUEST_EOVERFLOW); + ++semaphore->count; semaphore->available.NotifyOne(); return 0; } diff --git a/core/libs/prx/libkernel/Pthread/include/Cond.hpp b/core/libs/prx/libkernel/Pthread/include/Cond.hpp index 936b957149..de69c30f51 100644 --- a/core/libs/prx/libkernel/Pthread/include/Cond.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Cond.hpp @@ -6,6 +6,7 @@ class CondOperations { public: static int AbsoluteTimedwait(PthreadCond* cond, PthreadMutex* mutex, const KernelTimespec* abstime); + static int SetName(PthreadCond* cond, const char* name); }; extern "C" { diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index 0e83376197..0078e2b5f1 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -110,6 +110,7 @@ struct PthreadAttrPrivate { int _schedpriority; int _schedpolicy; int _inheritsched; + bool _createSuspended = false; KernelCpumask _affinity = DEFAULT_THREAD_AFFINITY; bool _affinityExplicit = false; std::size_t _guardsize = 0x1000; @@ -130,7 +131,10 @@ struct PthreadPrivate { std::atomic references{2}; std::atomic inWait{false}; std::atomic pendingException{0}; + std::atomic suspended{false}; + bool createdSuspended = false; std::atomic schedPolicy{1}; + int inheritSched = 4; void* wakeEvent = nullptr; void* stackAddress = nullptr; std::size_t stackSize = 0; @@ -179,6 +183,8 @@ void SetLiveGuestThreadsAffinity(KernelCpumask mask); KernelCpumask LiveGuestThreadsAffinity(); bool GuestThreadIsLive(const PthreadPrivate* thread); std::unique_lock LockLiveGuestThread(const PthreadPrivate* thread); +extern "C" int ResumeGuestThread_nid_no_patch(PthreadPrivate* thread); +extern "C" void ResumeAllGuestThreads_nid_no_patch(); bool LiveGuestThreadExists(std::int64_t tid); bool GetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask& mask); bool GetLiveGuestThreadSched_nid_no_patch(std::int64_t tid, int& policy, int& priority); diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index 0c26eb6762..7a7d3371af 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -97,7 +97,8 @@ int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr) { (*attr)->_detachstate = thread->_detached ? DETACH_DETACHED : DETACH_JOINABLE; (*attr)->_schedpriority = thread->priority.load(std::memory_order_relaxed); (*attr)->_schedpolicy = thread->schedPolicy.load(std::memory_order_relaxed); - (*attr)->_inheritsched = 4; + (*attr)->_inheritsched = thread->inheritSched; + (*attr)->_createSuspended = thread->createdSuspended; (*attr)->_affinity = thread->affinity.load(std::memory_order_relaxed); (*attr)->_affinityExplicit = true; (*attr)->_scope = thread->scope; @@ -124,8 +125,7 @@ int APS5_VABI scePthreadAttrGetguardsize(const PthreadAttr* attr, size_t* guard_ } int APS5_VABI scePthreadAttrGetinheritsched(const PthreadAttr* attr, int* inheritSched) { - if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; - if (!inheritSched) throw std::runtime_error(std::string(__func__) + ": null output"); + if (!attr || !*attr || !inheritSched) return SCE_KERNEL_ERROR_EINVAL; *inheritSched = (*attr)->_inheritsched; return SCE_OK; } diff --git a/core/libs/prx/libkernel/Pthread/src/Cond.cpp b/core/libs/prx/libkernel/Pthread/src/Cond.cpp index a3a3087bf7..2b4743e877 100644 --- a/core/libs/prx/libkernel/Pthread/src/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Cond.cpp @@ -25,7 +25,7 @@ std::mutex& InitializationMutex() { } PthreadCond destroyedCond() { - return reinterpret_cast(std::uintptr_t{2}); + return reinterpret_cast(std::uintptr_t{1}); } PthreadMutex destroyedMutex() { @@ -36,10 +36,9 @@ PthreadMutex adaptiveInitializer() { return reinterpret_cast(std::uintptr_t{1}); } -std::shared_ptr resolveCond(PthreadCond* cond, int* clockId = nullptr) { +PthreadCond resolveLocked(PthreadCond* cond) { if (!cond) throw std::invalid_argument("Condition variable pointer is null"); - std::lock_guard lock(InitializationMutex()); std::atomic_ref slot(*cond); auto current = slot.load(std::memory_order_acquire); if (current == destroyedCond()) @@ -48,6 +47,14 @@ std::shared_ptr resolveCond(PthreadCond* cond, int* clockI current = new PthreadCondPrivate(); slot.store(current, std::memory_order_release); } + return current; +} + +std::shared_ptr resolveCond(PthreadCond* cond, int* clockId = nullptr) { + std::lock_guard lock(InitializationMutex()); + const auto current = resolveLocked(cond); + if (!current) + return nullptr; if (clockId) *clockId = current->_clockid; return current->_cv; } @@ -136,6 +143,16 @@ int CondOperations::AbsoluteTimedwait(PthreadCond* cond, PthreadMutex* mutex, co return waitUntil(std::move(condition), mutex, TimedWait::DeadlineNanos(usec), __builtin_return_address(0)); } +int CondOperations::SetName(PthreadCond* cond, const char* name) { + if (!name) + throw std::invalid_argument("Condition variable name is null"); + std::lock_guard lock(InitializationMutex()); + const auto current = resolveLocked(cond); + if (!current) + return SCE_KERNEL_ERROR_EINVAL; + return 0; +} + extern "C" { int APS5_VABI scePthreadCondattrInit(PthreadCondattr* attr) { @@ -153,7 +170,9 @@ int APS5_VABI scePthreadCondattrDestroy(PthreadCondattr* attr) { } int APS5_VABI scePthreadCondattrSetclock(PthreadCondattr* attr, KernelClockid clockId) { + constexpr KernelClockid realtime = 0, virtualTime = 1, profiling = 2, monotonic = 4; if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + if (clockId != realtime && clockId != virtualTime && clockId != profiling && clockId != monotonic) return SCE_KERNEL_ERROR_EINVAL; (*attr)->_clockid = static_cast(clockId); return 0; } @@ -175,6 +194,8 @@ int APS5_VABI scePthreadCondDestroy(PthreadCond* cond) { std::lock_guard lock(InitializationMutex()); if (*cond == destroyedCond()) return SCE_KERNEL_ERROR_EINVAL; + if (*cond == nullptr) + return 0; delete *cond; std::atomic_ref(*cond).store(destroyedCond(), std::memory_order_release); return 0; diff --git a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp index f57ea97b25..62e5e38db1 100644 --- a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp @@ -1,8 +1,10 @@ #include "prx/libkernel/Pthread/include/Mutex.hpp" +#include "prx/libkernel/include/Api.hpp" #include "../include/Pthread.hpp" #include "../include/Mutex.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +#include "prx/libc/include/General.hpp" #include #include #include @@ -10,6 +12,7 @@ #include #include #include +#include namespace { @@ -17,6 +20,7 @@ constexpr int sceNotPermitted = static_cast(0x80020001u); constexpr int sceDeadlock = static_cast(0x8002000bu); constexpr int sceBusy = static_cast(0x80020010u); constexpr int sceTimedOut = static_cast(0x8002003cu); +constexpr std::int32_t posixFailure = -1; std::mutex& InitializationMutex() { static std::mutex mutex; return mutex; @@ -137,6 +141,17 @@ int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr) { return 0; } +int APS5_VABI scePthreadMutexattrSetkind(PthreadMutexattr* attr, int kind) { + if (!attr || !*attr) return sceKernelError(posixFailure); + if (kind < static_cast(MutexType::ErrorCheck) || kind > static_cast(MutexType::Adaptive)) + NotImplemented_nid_no_patch("scePthreadMutexattrSetkind with a kind other than 1 to 4"); + return scePthreadMutexattrSettype(attr, kind); +} + +int APS5_VABI scePthreadMutexattrGetkind(PthreadMutexattr attr) { + return sceKernelError(attr ? static_cast(attr->type) : posixFailure); +} + int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type) { if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; @@ -220,6 +235,10 @@ int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex) { int APS5_VABI scePthreadMutexTimedlock(PthreadMutex* mutex, KernelUseconds usec) { const auto deadline = TimedWait::DeadlineNanos(usec); auto* current = resolveMutex(mutex, true); + if (current && current->_type != MutexType::Recursive && current->_owner.load(std::memory_order_acquire) == std::this_thread::get_id()) { + TimedWait::SleepUntil(deadline); + return sceTimedOut; + } return acquireMutex(current, [=](auto& native) { return TimedWait::AcquireUntil(current, deadline, [&] { return native.try_lock(); }, [&](std::uint64_t micros) { return native.try_lock_for(std::chrono::microseconds(micros)); }); }, sceTimedOut, false); diff --git a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp index f502a5951e..8d06b7e9ea 100644 --- a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp @@ -105,8 +105,11 @@ int APS5_VABI scePthreadRwlockTryrdlock(PthreadRwlock* rwlock) { int APS5_VABI scePthreadRwlockTimedrdlock(PthreadRwlock* rwlock, KernelUseconds usec) { auto* lock = RequireRwlock(rwlock, __func__); if (!lock) return SCE_KERNEL_ERROR_EINVAL; - if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; const std::uint64_t deadline = TimedWait::DeadlineNanos(usec); + if (OwnsWrite(lock)) { + TimedWait::SleepUntil(deadline); + return SCE_KERNEL_ERROR_ETIMEDOUT; + } std::unique_lock state(lock->_state); if (!lock->_changed.WaitUntil(state, deadline, [&] { return ReaderMayEnter(lock); })) return SCE_KERNEL_ERROR_ETIMEDOUT; return EnterRead(lock); @@ -134,8 +137,11 @@ int APS5_VABI scePthreadRwlockTrywrlock(PthreadRwlock* rwlock) { int APS5_VABI scePthreadRwlockTimedwrlock(PthreadRwlock* rwlock, KernelUseconds usec) { auto* lock = RequireRwlock(rwlock, __func__); if (!lock) return SCE_KERNEL_ERROR_EINVAL; - if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; const std::uint64_t deadline = TimedWait::DeadlineNanos(usec); + if (OwnsWrite(lock)) { + TimedWait::SleepUntil(deadline); + return SCE_KERNEL_ERROR_ETIMEDOUT; + } std::unique_lock state(lock->_state); ++lock->_waitingWriters; const bool acquired = lock->_changed.WaitUntil(state, deadline, [&] { return WriterMayEnter(lock); }); diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 2191cce204..b9d2906a25 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -37,6 +37,20 @@ #include #endif +#ifdef __linux__ +#include +#else +static const auto initialThreadId = std::this_thread::get_id(); +#endif + +extern "C" bool IsInitialPthread_nid_no_patch() { +#ifdef __linux__ + return ::syscall(SYS_gettid) == ::getpid(); +#else + return std::this_thread::get_id() == initialThreadId; +#endif +} + static constexpr int SCE_OK = 0; static constexpr std::size_t GUEST_THREAD_NAME_CAPACITY = 32; @@ -474,6 +488,7 @@ PthreadPrivate::PthreadPrivate(const PthreadAttrPrivate* attr) : _finished(false affinity.store(attr && attr->_affinityExplicit ? attr->_affinity : inherited, std::memory_order_relaxed); constexpr int ExplicitSched = 0; const bool explicitSched = attr && attr->_inheritsched == ExplicitSched; + if (attr) inheritSched = attr->_inheritsched; const int inheritedPolicy = parent ? parent->schedPolicy.load(std::memory_order_relaxed) : schedPolicy.load(std::memory_order_relaxed); schedPolicy.store(explicitSched ? attr->_schedpolicy : inheritedPolicy, std::memory_order_relaxed); const int attrPriority = attr ? attr->_schedpriority : DEFAULT_THREAD_PRIORITY; @@ -541,6 +556,26 @@ bool GuestThreadIsLive(const PthreadPrivate* thread) { return LockLiveGuestThread(thread).owns_lock(); } +extern "C" int ResumeGuestThread_nid_no_patch(PthreadPrivate* thread) { + constexpr int GuestEsrch = 3; + const auto live = LockLiveGuestThread(thread); + if (!live.owns_lock()) return GuestEsrch; + thread->suspended.store(false, std::memory_order_release); + thread->suspended.notify_all(); + return 0; +} + +extern "C" void ResumeAllGuestThreads_nid_no_patch() { + const auto* self = CurrentGuestState().current; + auto& registry = CpuClocks(); + std::lock_guard lock(registry.lock); + for (const auto& [id, thread] : registry.threads) { + if (thread == self) continue; + thread->suspended.store(false, std::memory_order_release); + thread->suspended.notify_all(); + } +} + bool GetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask& mask) { auto& registry = CpuClocks(); std::lock_guard lock(registry.lock); @@ -761,6 +796,7 @@ static void RunThread(std::unique_ptr args) { const sigset_t signalMask = args->signalMask; #endif args.reset(); + self->suspended.wait(true, std::memory_order_acquire); #ifndef _WIN32 pthread_sigmask(SIG_SETMASK, &signalMask, nullptr); #endif @@ -936,6 +972,8 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread p->scope = CurrentGuestState().current->scope; } if (name) p->name = name; + p->createdSuspended = attr && (*attr)->_createSuspended; + p->suspended.store(p->createdSuspended, std::memory_order_relaxed); std::promise start; auto args = std::make_unique(ThreadArgs{entry, arg, p.get()}); #ifdef _WIN32 diff --git a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp index 18a966c5da..44990b4654 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp @@ -23,6 +23,8 @@ struct SocketOptionState { unsigned int tcpKeepInterval = 0; unsigned int tcpKeepCount = 0; int keptFlags = 0; + int tcpFlags = 0; + int ipReceiveOptions = 0; std::uint32_t multicastInterface = 0; std::vector memberships; }; diff --git a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp index d59cfc4304..c5343daede 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp @@ -41,3 +41,4 @@ std::int64_t Writev(int descriptor, const Iovec* iov, int iovcnt); } extern "C" bool GuestSocketIsOpen_nid_no_patch(int descriptor); +extern "C" bool GuestSocketReadGuest_nid_no_patch(void* destination, const void* source, std::size_t bytes); diff --git a/core/libs/prx/libkernel/Socket/src/Pselect.cpp b/core/libs/prx/libkernel/Socket/src/Pselect.cpp index 8ae40d8549..f30cde4a91 100644 --- a/core/libs/prx/libkernel/Socket/src/Pselect.cpp +++ b/core/libs/prx/libkernel/Socket/src/Pselect.cpp @@ -1,31 +1,69 @@ #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 SetMask = 3; +constexpr int Eintr = 4; +constexpr int DescriptorLimit = 1024; + +struct Timeval { + std::int64_t seconds; + std::int64_t microseconds; +}; + +using SavedSet = std::array; + +void Save(SavedSet& saved, const void* set, std::size_t bytes) { + if (set != nullptr) std::memcpy(saved.data(), set, bytes); +} + +void Restore(void* set, const SavedSet& saved, std::size_t bytes) { + if (set != nullptr) std::memcpy(set, saved.data(), bytes); +} + +int ScanAfterCaughtSignal(int nfds, void* readfds, void* writefds, void* exceptfds, const Timeval* wait) { + const bool validWait = wait == nullptr || (wait->seconds >= 0 && wait->microseconds >= 0 && wait->microseconds < 1000000); + if (nfds < 0 || nfds > DescriptorLimit || !validWait) return select_nid_postfix(nfds, readfds, writefds, exceptfds, wait); + const std::size_t bytes = static_cast((nfds + 63) / 64) * sizeof(std::uint64_t); + SavedSet read{}, write{}, except{}; + Save(read, readfds, bytes); + Save(write, writefds, bytes); + Save(except, exceptfds, bytes); + const Timeval zero{}; + const int result = select_nid_postfix(nfds, readfds, writefds, exceptfds, &zero); + if (result != 0 || (wait != nullptr && wait->seconds == 0 && wait->microseconds == 0)) return result; + Restore(readfds, read, bytes); + Restore(writefds, write, bytes); + Restore(exceptfds, except, bytes); + *__error_nid_postfix() = Eintr; + return -1; +} + +} extern "C" { int APS5_VABI pselect_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout, const void* sigmask) { - constexpr int SetMask = 3; - struct Timeval { - std::int64_t seconds; - std::int64_t microseconds; - } limit{}; + Timeval limit{}; const Timeval* wait = nullptr; if (timeout != nullptr) { const auto* spec = static_cast(timeout); limit = {spec[0], spec[1] / 1000}; wait = &limit; } - std::uint32_t previous[4]{}; - if (sigmask != nullptr && sigprocmask_nid_postfix(SetMask, sigmask, previous) != 0) return -1; - const int result = select_nid_postfix(nfds, readfds, writefds, exceptfds, wait); - if (sigmask != nullptr) { - const int error = *__error_nid_postfix(); - sigprocmask_nid_postfix(SetMask, previous, nullptr); - - *__error_nid_postfix() = error; - } + if (sigmask == nullptr) return select_nid_postfix(nfds, readfds, writefds, exceptfds, wait); + GuestSignalSet previous{}; + const bool caught = SetSignalMaskCatching_nid_no_patch(static_cast(sigmask), &previous) != 0; + const int result = caught ? ScanAfterCaughtSignal(nfds, readfds, writefds, exceptfds, wait) + : select_nid_postfix(nfds, readfds, writefds, exceptfds, wait); + const int error = *__error_nid_postfix(); + sigprocmask_nid_postfix(SetMask, &previous, nullptr); + *__error_nid_postfix() = error; return result; } diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index e27ce66b87..399e530a79 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -51,6 +51,14 @@ void ClearDescriptor(void* set, int descriptor) { if (set != nullptr) static_cast(set)[descriptor / 64] &= ~(std::uint64_t{1} << (descriptor % 64)); } +void ClearDescriptorsFrom(int nfds, void* readfds, void* writefds, void* exceptfds) { + if (nfds % 64 == 0) return; + const std::uint64_t kept = (std::uint64_t{1} << (nfds % 64)) - 1; + for (void* set : {readfds, writefds, exceptfds}) { + if (set != nullptr) static_cast(set)[nfds / 64] &= kept; + } +} + struct PollGroup { KernelSocketPoll::Poller poll; std::vector entries; @@ -207,8 +215,8 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* kqueues.slots.push_back(requested.size()); requested.push_back({descriptor, events, 0}); } else if (File::IsRandomDevice(descriptor)) devices.push_back({descriptor, events, static_cast((events & KernelSocketPoll::Readable) != 0 ? KernelSocketPoll::Readable : events)}); - else if (descriptor <= 2) devices.push_back({descriptor, events, static_cast(descriptor == 0 ? 0 : events & KernelSocketPoll::Writable)}); else if (File::IsVnode(descriptor)) devices.push_back({descriptor, events, static_cast(events & (KernelSocketPoll::Readable | KernelSocketPoll::Writable))}); + else if (descriptor <= 2) devices.push_back({descriptor, events, static_cast(descriptor == 0 ? 0 : events & KernelSocketPoll::Writable)}); else { net.entries.push_back({descriptor, events, 0}); net.slots.push_back(requested.size()); @@ -311,6 +319,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* if ((entry.revents & (KernelSocketPoll::Writable | KernelSocketPoll::Error)) == 0) ClearDescriptor(writefds, entry.descriptor); if ((entry.revents & KernelSocketPoll::Urgent) == 0) ClearDescriptor(exceptfds, entry.descriptor); } + ClearDescriptorsFrom(nfds, readfds, writefds, exceptfds); return ready; } if (limit != nullptr && std::chrono::steady_clock::now() >= deadline) { @@ -319,6 +328,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* ClearDescriptor(writefds, entry.descriptor); ClearDescriptor(exceptfds, entry.descriptor); } + ClearDescriptorsFrom(nfds, readfds, writefds, exceptfds); return 0; } } diff --git a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp index fd7ea3cfd8..ff0fb47fb7 100644 --- a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp +++ b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp @@ -49,6 +49,9 @@ constexpr int GuestTcpMaximumSegment = 2; constexpr int GuestTcpKeepIdle = 0x100; constexpr int GuestTcpKeepInterval = 0x200; constexpr int GuestTcpKeepCount = 0x400; +constexpr int GuestTcpNoPush = 4; +constexpr int GuestTcpNoOptions = 8; +constexpr int GuestTcpOption80 = 0x80; constexpr int MinimumSegment = 216; constexpr unsigned int TicksPerSecond = 1000; constexpr unsigned int DefaultKeepIdle = 7200; @@ -62,6 +65,9 @@ constexpr int GuestIpMulticastLoop = 11; constexpr int GuestIpMulticastInterface = 9; constexpr int GuestIpAddMembership = 12; constexpr int GuestIpDropMembership = 13; +constexpr int GuestIpReceiveDestination = 7; +constexpr int GuestIpReceiveInterface = 20; +constexpr int GuestIpReceiveTimeToLive = 65; constexpr std::uint32_t MulticastRequestWithIndex = 12; constexpr int ErrorNotSupported = 45; constexpr int ErrorAddressInUse = 48; @@ -130,6 +136,24 @@ void EnsureBuffers(SocketOptionState& state, int family, int type) { state.receiveLowWater = 1; } +int TcpFlagBit(int option) { + switch (option) { + case GuestTcpNoPush: return 0x1000; + case GuestTcpNoOptions: return 0x8; + case GuestTcpOption80: return 0x10000; + default: return 0; + } +} + +int IpReceiveBit(int option) { + switch (option) { + case GuestIpReceiveDestination: return 1; + case GuestIpReceiveInterface: return 2; + case GuestIpReceiveTimeToLive: return 4; + default: return 0; + } +} + 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; if (count != 0) std::memcpy(output, value, count); @@ -298,18 +322,28 @@ int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int fa return Copy(value, length, &number, sizeof(number)); } +bool HasPeer(std::uintptr_t native) { + sockaddr_storage peer{}; + socklen_t size = sizeof(peer); + return ::getpeername(static_cast(native), reinterpret_cast(&peer), &size) == 0; +} + int SetTcpLevelOption(std::uintptr_t native, SocketOptionState& state, bool tcp, int option, const void* value, std::uint32_t length) { if (!tcp) return ErrorInvalid; if (option != GuestTcpNoDelay && option != GuestTcpMaximumSegment && option != GuestTcpKeepIdle && option != GuestTcpKeepInterval && - option != GuestTcpKeepCount) + option != GuestTcpKeepCount && TcpFlagBit(option) == 0) return ErrorNoProtocolOption; if (length < sizeof(int)) return ErrorInvalid; int number; std::memcpy(&number, value, sizeof(number)); + if (const int bit = TcpFlagBit(option); bit != 0) { + state.tcpFlags = number != 0 ? state.tcpFlags | bit : state.tcpFlags & ~bit; + return 0; + } const auto unsignedNumber = static_cast(number); if (option == GuestTcpMaximumSegment) { - if (number <= 0 || number > state.tcpMaximumSegment || number + 40 < MinimumSegment) return ErrorInvalid; - state.tcpMaximumSegment = number; + if (!HasPeer(native) || number <= 0 || number > state.tcpMaximumSegment || number + 40 < MinimumSegment) return ErrorInvalid; + state.tcpMaximumSegment = std::max(number, MinimumSegment); return 0; } if (option == GuestTcpKeepCount) { @@ -335,6 +369,10 @@ int GetTcpLevelOption(std::uintptr_t native, SocketOptionState& state, bool tcp, : (state.tcpKeepCount != 0 ? state.tcpKeepCount : DefaultKeepCount); return Copy(value, length, &number, sizeof(number)); } + if (const int bit = TcpFlagBit(option); bit != 0) { + const int number = state.tcpFlags & bit; + return Copy(value, length, &number, sizeof(number)); + } if (option != GuestTcpNoDelay) return ErrorNoProtocolOption; int enabled = 0; socklen_t size = sizeof(enabled); @@ -396,6 +434,13 @@ int SetIpLevelOption(std::uintptr_t native, SocketOptionState& state, bool ipv4, std::memcpy(&state.multicastInterface, &address, sizeof(address)); return 0; } + if (const int bit = IpReceiveBit(option); bit != 0) { + if (length < sizeof(int)) return ErrorInvalid; + int number; + std::memcpy(&number, value, sizeof(number)); + 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; @@ -438,7 +483,8 @@ int GetIpLevelOption(SocketOptionState& state, bool ipv4, bool datagram, int opt return Copy(value, length, &state.multicastInterface, sizeof(state.multicastInterface)); } int number; - if (option == GuestIpTimeToLive) number = state.ipTimeToLive; + if (const int bit = IpReceiveBit(option); bit != 0) number = (state.ipReceiveOptions & bit) != 0 ? 1 : 0; + else if (option == GuestIpTimeToLive) number = state.ipTimeToLive; else if (option == GuestIpTypeOfService) number = state.ipTypeOfService; else if (option == GuestIpMulticastTimeToLive) number = state.multicastTimeToLive; else if (option == GuestIpMulticastLoop) number = state.multicastLoop ? 1 : 0; diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 436e6fdee0..60537e9e25 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -4,6 +4,7 @@ #define WIN32_LEAN_AND_MEAN #include #include +#include #include #include #else @@ -40,6 +41,7 @@ #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" +#include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include "prx/libkernel/Socket/include/SocketConnect.hpp" @@ -123,6 +125,7 @@ struct Socket : KernelSocketPoll::NativeOwner { bool receiveShut = false; bool sendShut = false; std::uint32_t inode = 0; + std::weak_ptr peer; Socket(NativeSocket value, int family, int type) : value(value), family(family), type(type) {} ~Socket() { if (value == Invalid) return; @@ -273,11 +276,14 @@ int AddressLengthError(const void* input, std::uint32_t length) { if (length < 2) return 22; return input ? 0 : 14; } -bool Address(const void* input, std::uint32_t length, sockaddr_storage& native, socklen_t& size) { +bool Address(int family, const void* input, std::uint32_t length, sockaddr_storage& native, socklen_t& size, bool lengthFirst = false) { if (const int error = AddressLengthError(input, length)) { Fail(error); return false; } const auto* bytes = static_cast(input); - if ((bytes[1] == 2 && length < 16) || (bytes[1] == 28 && length < 28)) { Fail(22); return false; } - if (bytes[1] == 2 && length >= 16) { + const bool wrongLength = (family == 2 && length != 16) || (family == 28 && length != 28); + if (lengthFirst && wrongLength) { Fail(22); return false; } + if (family == 2 && bytes[1] != 2) { Fail(47); return false; } + if (wrongLength) { Fail(22); return false; } + if (family == 2) { auto& v4 = reinterpret_cast(native); v4.sin_family = AF_INET; std::memcpy(&v4.sin_port, bytes + 2, 2); @@ -285,7 +291,7 @@ bool Address(const void* input, std::uint32_t length, sockaddr_storage& native, size = sizeof(v4); return true; } - if (bytes[1] == 28 && length >= 28) { + if (family == 28 && bytes[1] == 28) { auto& v6 = reinterpret_cast(native); v6.sin6_family = AF_INET6; std::memcpy(&v6.sin6_port, bytes + 2, 2); @@ -298,14 +304,15 @@ bool Address(const void* input, std::uint32_t length, sockaddr_storage& native, Fail(47); return false; } -int LongAddressError(const Socket& socket, const void* address, std::uint32_t length) { - if ((socket.family != 2 && socket.family != 28) || length <= (socket.family == 2 ? 16u : 28u)) return 0; +int LongMappedDualStack(const Socket& socket, const void* address, std::uint32_t length) { + if (socket.family != 28 || socket.type != 2 || address == nullptr || length <= 28 || length > 255) return 0; + const auto* bytes = static_cast(address); constexpr unsigned char mapped[12] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}; - if (socket.family != 28 || socket.type != 2 || std::memcmp(static_cast(address) + 8, mapped, sizeof(mapped)) != 0) return 22; + if (bytes[1] != 28 || std::memcmp(bytes + 8, mapped, sizeof(mapped)) != 0) return 0; int ipv6Only = 0; socklen_t size = sizeof(ipv6Only); - if (::getsockopt(socket.value, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&ipv6Only), &size)) return NativeError(); - return ipv6Only ? 22 : 0; + if (::getsockopt(socket.value, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&ipv6Only), &size)) return Fail(NativeError()); + return ipv6Only ? 0 : 1; } std::uint16_t& PortOf(sockaddr_storage& address) { return address.ss_family == AF_INET ? reinterpret_cast(address).sin_port : reinterpret_cast(address).sin6_port; @@ -314,6 +321,11 @@ bool Listening(const Socket& socket) { bool listening = false; return GuestSockets::NativeListening(static_cast(socket.value), listening) && listening; } +bool PeerConnected(const Socket& socket) { + sockaddr_storage peer{}; + socklen_t size = sizeof(peer); + return ::getpeername(socket.value, reinterpret_cast(&peer), &size) == 0; +} 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; @@ -1181,7 +1193,7 @@ int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { if (family != 2 && family != 28) return Fail(47); const int nativeType = plainType == 1 ? SOCK_STREAM : plainType == 2 ? SOCK_DGRAM : -1; const int expectedProtocol = plainType == 1 ? 6 : 17; - const bool inetKnowsType = plainType == 3 || plainType == 5; + const bool inetKnowsType = plainType == 3; if (nativeType == -1) return Fail(protocol == 0 && plainType != 0 && !inetKnowsType ? 41 : 43); if (protocol != 0 && protocol != expectedProtocol) return Fail(43); #ifdef _WIN32 @@ -1194,9 +1206,17 @@ int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { const int ipv6Only = 1; if (family == 28 && ::setsockopt(native, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&ipv6Only), sizeof(ipv6Only))) return Fail(NativeError()); +#ifdef _WIN32 + BOOL reportUnreachable = FALSE; + DWORD returned = 0; + if (nativeType == SOCK_DGRAM && WSAIoctl(native, SIO_UDP_CONNRESET, &reportUnreachable, sizeof(reportUnreachable), nullptr, 0, + &returned, nullptr, nullptr) != 0) + return Fail(NativeError()); +#endif try { auto socket = std::make_shared(native, family, plainType); guard.value = Invalid; + if (family == 28) socket->options.tcpMaximumSegment = 1220; if (nonblocking && SetNonBlocking(*socket)) return -1; const int number = Install(std::move(socket)); if (number >= 0 && (type & GuestCloseOnExec) != 0) { @@ -1235,6 +1255,8 @@ int APS5_VABI socketpair_nid_postfix(int family, int type, int protocol, int* de firstGuard.value = Invalid; auto second = std::make_shared(native[1], GuestUnix, plainType); secondGuard.value = Invalid; + first->peer = second; + second->peer = first; if (nonblocking && (SetNonBlocking(*first) || SetNonBlocking(*second))) return -1; const int firstDescriptor = Install(std::move(first)); if (firstDescriptor < 0) return -1; @@ -1263,16 +1285,16 @@ int APS5_VABI connect_nid_postfix(int descriptor, const void* address, std::uint if (socket->family != GuestUnix && socket->type == 2 && address && length >= 2 && static_cast(address)[1] == 0 && Disconnect(*socket) != 0) return -1; + if (socket->type == 1 && Listening(*socket)) return Fail(45); + const int mapped = LongMappedDualStack(*socket, address, length); + if (mapped < 0) return -1; sockaddr_storage native{}; socklen_t size; - if (!Address(address, length, native, size)) return -1; + if (!Address(socket->family, address, mapped ? 28 : length, native, size)) return -1; #ifdef _WIN32 if (socket->type == 1) RefuseClosedLoopbackAtOnce_nid_no_patch(socket->value, reinterpret_cast(&native)); #endif - if (socket->type == 1 && Listening(*socket)) return Fail(45); - if (const int error = LongAddressError(*socket, address, length)) return Fail(error); if (ZeroPortConnect(*socket, address, length)) return -1; - if (native.ss_family != (socket->family == 2 ? AF_INET : AF_INET6)) return Fail(47); { std::lock_guard lock(socket->modeMutex); socket->options.reportedError = 0; @@ -1300,6 +1322,7 @@ int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* l try { auto accepted = std::make_shared(native, listener->family, listener->type); guard.value = Invalid; + if (listener->family == 28) accepted->options.tcpMaximumSegment = 1220; bool inheritNonblocking; { std::lock_guard modeLock(listener->modeMutex); @@ -1317,6 +1340,7 @@ int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* l accepted->options.tcpKeepIdle = listener->options.tcpKeepIdle; accepted->options.tcpKeepInterval = listener->options.tcpKeepInterval; accepted->options.tcpKeepCount = listener->options.tcpKeepCount; + accepted->options.tcpFlags = listener->options.tcpFlags & 0x1000; } if (inheritNonblocking && SetNonBlocking(*accepted)) return -1; const int acceptedDescriptor = Install(std::move(accepted)); @@ -1360,15 +1384,35 @@ int APS5_VABI bind_nid_postfix(int descriptor, const void* address, std::uint32_ if (!socket) return -1; sockaddr_storage native{}; socklen_t size; - if (!Address(address, length, native, size)) return -1; - if (const int error = LongAddressError(*socket, address, length)) return Fail(error); + const int mapped = LongMappedDualStack(*socket, address, length); + if (mapped < 0) return -1; + if (!Address(socket->family, address, mapped ? 28 : length, native, size)) return -1; return ::bind(socket->value, reinterpret_cast(&native), size) ? Fail(NativeError()) : 0; } +namespace { +bool AlreadyShut(NativeSocket socket, int how) { +#ifdef __APPLE__ + if (errno != ENOTCONN) return false; + sockaddr_storage peer{}; + socklen_t size = sizeof(peer); + if (::getpeername(socket, reinterpret_cast(&peer), &size)) { + errno = ENOTCONN; + return false; + } + return how != SHUT_RDWR || ::shutdown(socket, SHUT_WR) == 0 || errno == ENOTCONN; +#else + (void)socket; + (void)how; + return false; +#endif +} +} + int APS5_VABI shutdown_nid_postfix(int descriptor, int how) { const auto socket = Lookup(descriptor); if (!socket) return -1; if (how < 0 || how > 2) return Fail(22); - if (::shutdown(socket->value, how)) return Fail(NativeError()); + if (::shutdown(socket->value, how) && !AlreadyShut(socket->value, how)) return Fail(NativeError()); std::lock_guard lock(socket->modeMutex); if (how != 1) socket->receiveShut = true; if (how != 0) socket->sendShut = true; @@ -1396,6 +1440,7 @@ int APS5_VABI getsockname_nid_postfix(int descriptor, void* address, std::uint32 int APS5_VABI getpeername_nid_postfix(int descriptor, void* address, std::uint32_t* length) { const auto socket = Lookup(descriptor); if (!socket) return -1; + if (socket->family == GuestUnix && socket->peer.expired()) return Fail(57); if (!address || !length) return Fail(14); if (socket->family == GuestUnix) return UnnamedAddress(address, length); sockaddr_storage native{}; @@ -1441,15 +1486,13 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st if (!buffer && length) return Fail(14); if (const int waited = WaitForTransfer(*socket, true, flags)) return Fail(waited); int result; - if (!address) { - if (addressLength != 0) return Fail(22); + if (!address || (socket->type == 1 && PeerConnected(*socket))) { + if (!address && addressLength != 0) return Fail(22); result = ::send(socket->value, static_cast(buffer), static_cast(length), SocketSendFlags(*socket, flags)); } else { sockaddr_storage native{}; socklen_t size; - if (!Address(address, addressLength, native, size)) return -1; - if (socket->family == 28 && socket->type == 2 && addressLength > 28) return Fail(22); - if (native.ss_family != (socket->family == 2 ? AF_INET : AF_INET6)) return Fail(47); + if (!Address(socket->family, address, addressLength, native, size, true)) return -1; if (const auto answered = ZeroPortSend(*socket, native, length); answered != NotAnswered) return answered; result = static_cast(::sendto(socket->value, static_cast(buffer), static_cast(length), SocketSendFlags(*socket, flags), reinterpret_cast(&native), size)); @@ -1688,6 +1731,12 @@ int APS5_VABI sendfile_nid_postfix(int file, int descriptor, std::int64_t offset if (offset < 0) return Fail(22); if (headers && (!SendfileIovecs(headers->headers, headers->headerCount) || !SendfileIovecs(headers->trailers, headers->trailerCount))) return Fail(22); + std::int64_t sent = 0; + const auto finish = [&](int error) { + if (sbytes) *sbytes = sent; + return error ? Fail(error) : 0; + }; + if (DescriptorIsOpen_nid_no_patch(file) && !File::IsVnode(file) && !File::IsSharedMemoryDescriptor_nid_no_patch(file)) return finish(22); std::vector chunk; try { chunk.resize(0x10000); @@ -1701,11 +1750,6 @@ int APS5_VABI sendfile_nid_postfix(int file, int descriptor, std::int64_t offset std::uint64_t remaining = length ? length : UINT64_MAX; auto received = read(offset, remaining); if (received == -9) return Fail(9); - std::int64_t sent = 0; - const auto finish = [&](int error) { - if (sbytes) *sbytes = sent; - return error ? Fail(error) : 0; - }; if (received < 0) return finish(received == -21 || received == -29 ? 22 : static_cast(-received)); const auto socket = Lookup(descriptor); if (!socket) return finish(*__error_nid_postfix()); @@ -1763,13 +1807,13 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c if (entry.revents) ++ready; continue; } - if (entry.descriptor <= 2) { - entry.revents = static_cast(entry.descriptor == 0 ? 0 : entry.events & GuestPollWritable); + if (File::IsVnode(entry.descriptor)) { + entry.revents = static_cast(entry.events & GuestPollVnodeReady); if (entry.revents) ++ready; continue; } - if (File::IsVnode(entry.descriptor)) { - entry.revents = static_cast(entry.events & GuestPollVnodeReady); + if (entry.descriptor <= 2) { + entry.revents = static_cast(entry.descriptor == 0 ? 0 : entry.events & GuestPollWritable); if (entry.revents) ++ready; continue; } @@ -1821,6 +1865,9 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c if (pipe && (entry.revents & GuestPollError)) entry.revents = static_cast((entry.revents & ~GuestPollError) | GuestPollHangUp); if (pipe && (entry.revents & GuestPollHangUp)) entry.revents = static_cast((entry.revents & ~GuestPollWritable) | (entry.events & GuestPollReadable)); +#ifndef _WIN32 + if (entry.revents & GuestPollHangUp) entry.revents = static_cast(entry.revents & ~GuestPollWritable); +#endif if (entry.revents) ++ready; }; const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout < 0 ? 0 : timeout); @@ -1886,6 +1933,10 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c } } +extern "C" bool GuestSocketReadGuest_nid_no_patch(void* destination, const void* source, std::size_t bytes) { + return ::ReadGuest(destination, source, bytes); +} + std::int64_t GuestSockets::Read(int descriptor, void* buffer, std::size_t length) { if (length == 0) return IsOpen(descriptor) ? 0 : Fail(9); return recv_nid_postfix(descriptor, buffer, std::min(length, INT_MAX), 0); diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index 8882083584..73b40090f1 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -681,6 +681,7 @@ int APS5_VABI sceKernelRaiseException(Pthread thread, int signum) { const auto handler = Handler(signum); if (handler == nullptr) throw std::runtime_error("sceKernelRaiseException: no handler installed for the signal"); #ifdef _WIN32 + if (thread->suspended.load(std::memory_order_acquire)) throw std::runtime_error("sceKernelRaiseException: the target thread is still suspended at creation"); return RaiseOn(thread, handler, signum) ? 0 : SCE_KERNEL_ERROR_ESRCH; #else return RaiseOn(thread) ? 0 : SCE_KERNEL_ERROR_ESRCH; diff --git a/core/libs/prx/libkernel/System/src/Getlogin.cpp b/core/libs/prx/libkernel/System/src/Getlogin.cpp new file mode 100644 index 0000000000..c795555bcb --- /dev/null +++ b/core/libs/prx/libkernel/System/src/Getlogin.cpp @@ -0,0 +1,12 @@ +#include "prx/libc/include/Api.hpp" +#include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +extern "C" char* APS5_VABI getlogin_nid_postfix(void) { + static char logname[33]; + if (const int error = getlogin_r_nid_postfix(logname, static_cast(sizeof(logname))); error != 0) { + *__error_nid_postfix() = error; + return nullptr; + } + return logname[0] != '\0' ? logname : nullptr; +} diff --git a/core/libs/prx/libkernel/System/src/Login.cpp b/core/libs/prx/libkernel/System/src/Login.cpp index e188559977..d25e16d5b2 100644 --- a/core/libs/prx/libkernel/System/src/Login.cpp +++ b/core/libs/prx/libkernel/System/src/Login.cpp @@ -3,6 +3,10 @@ #include #include #include +#include +#include +#include +#include #include #ifdef _WIN32 #include @@ -13,7 +17,16 @@ #endif namespace { +std::optional g_loginForTest; + int ReadLogin(char* output, int size) { + if (g_loginForTest) { + if (g_loginForTest->empty()) throw std::runtime_error("getlogin_r: host effective user has no account name"); + const std::size_t length = g_loginForTest->size() + 1; + if (length > static_cast(size)) return 34; + std::memcpy(output, g_loginForTest->c_str(), length); + return 0; + } #ifdef _WIN32 std::array name{}; DWORD count = static_cast(name.size()); @@ -44,9 +57,37 @@ int ReadLogin(char* output, int size) { } } +extern "C" void LoginNameForTest_nid_no_patch(const char* name) { + if (name == nullptr) g_loginForTest.reset(); + else g_loginForTest = name; +} + +namespace { +constexpr std::size_t MaxLoginName = 33; +std::mutex sessionMutex; +std::optional sessionLogin; +} + +extern "C" int SetLogin_nid_no_patch(const char* name) { + if (name == nullptr) return 14; + const std::size_t length = ::strnlen(name, MaxLoginName); + if (length == MaxLoginName) return 22; + std::lock_guard lock(sessionMutex); + sessionLogin.emplace(name, length); + return 0; +} + extern "C" int APS5_VABI getlogin_r_nid_postfix(char* output, int size) { if (size <= 0) return 34; if (output == nullptr) return 14; + { + std::lock_guard lock(sessionMutex); + if (sessionLogin) { + if (sessionLogin->size() + 1 > static_cast(size)) return 34; + std::memcpy(output, sessionLogin->c_str(), sessionLogin->size() + 1); + return 0; + } + } const int saved = errno; const int result = ReadLogin(output, size); errno = saved; diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 6532933db3..2840421c0b 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -17,12 +17,21 @@ #include #endif #endif +#if defined(__APPLE__) +#include +#endif #if defined(__linux__) && defined(__x86_64__) #include #include #endif +#ifdef _WIN32 +#include "prx/libc/include/ChildProcesses.hpp" +#else +#include +#endif #include "SceTypes.hpp" +#include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/HostEntropy.hpp" @@ -40,11 +49,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -53,6 +64,7 @@ #include #endif +extern char** environ; namespace { constexpr int sceInvalidArgument = static_cast(0x80020016u); @@ -61,10 +73,19 @@ constexpr int errnoNoSuchProcess = 3; constexpr int errnoNoChild = 10; constexpr int errnoBadAddress = 14; constexpr int errnoInvalidArgument = 22; +constexpr int errnoTooBig = 7; +constexpr int errnoNoExec = 8; +constexpr int errnoNoEnt = 2; +constexpr int errnoAccess = 13; +constexpr int errnoBadFile = 9; constexpr int freebsdGroupsMax = 1023; std::mutex supplementaryGroupsMutex; std::vector supplementaryGroups{0}; constexpr unsigned freebsdWaitOptions = 0x8000003Fu; +constexpr std::size_t ExecArgumentBytesLimit = 262144; +constexpr std::string_view ExecDefaultSearchPath = "/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"; +constexpr std::string_view ExecApplicationRoot = "/app0/"; +constexpr std::string_view ExecMainExecutable = "/app0/eboot.bin"; bool StoreGuestId(std::uint32_t* output) { if (output == nullptr) return true; @@ -278,6 +299,281 @@ std::mutex processLimitMutex; GuestResourceLimit guestProcessResourceLimit{guestProcessLimit, guestProcessLimit}; } +namespace { + +bool ExecProgramFound(std::string_view name, bool search, int& error, std::string& found) { + const auto executable = [&error, &found](const std::string& candidate) { + found = candidate; + std::filesystem::path native; + std::error_code probe; + const int resolved = ResolvePathError_nid_no_patch(candidate.c_str(), native); + if (resolved != 0 || !std::filesystem::exists(native, probe)) { + error = resolved == 0 ? NotFoundError_nid_no_patch(native) : resolved; + return false; + } + if (!std::filesystem::is_regular_file(native, probe) || !ExecutePermission_nid_no_patch(native)) { + error = errnoAccess; + return false; + } + return true; + }; + if (name.empty()) { + error = errnoNoEnt; + return false; + } + if (!search || name.find('/') != std::string_view::npos) return executable(std::string(name)); + bool inaccessible = false; + const char* variable = getenv_nid_postfix("PATH"); + std::string_view directories = variable != nullptr ? std::string_view(variable) : ExecDefaultSearchPath; + while (true) { + const auto separator = directories.find(':'); + const auto directory = directories.substr(0, separator); + const std::string candidate = (directory.empty() ? std::string(".") : std::string(directory)) + '/' + std::string(name); + found = candidate; + std::filesystem::path native; + std::error_code probe; + if (ResolvePathError_nid_no_patch(candidate.c_str(), native) == 0 && std::filesystem::exists(native, probe)) { + if (std::filesystem::is_regular_file(native, probe) && ExecutePermission_nid_no_patch(native)) return true; + inaccessible = true; + } + if (separator == std::string_view::npos) { + error = inaccessible ? errnoAccess : errnoNoEnt; + return false; + } + directories.remove_prefix(separator + 1); + } +} + +std::string ExecFlattenedName(const std::string& guest) { + std::string_view relative(guest); + if (!relative.starts_with(ExecApplicationRoot)) + NotImplemented_nid_no_patch((std::string("exec: executables outside /app0 (") + guest + ")").c_str()); + relative.remove_prefix(ExecApplicationRoot.size()); + std::string name; + while (true) { + const std::size_t separator = relative.find('/'); + const std::string_view component = relative.substr(0, separator); + if (component.empty() || component == "." || component == "..") + NotImplemented_nid_no_patch((std::string("exec: a path with an empty, . or .. component (") + guest + ")").c_str()); + name += (name.empty() ? "" : ".") + std::string(component); + if (separator == std::string_view::npos) return name; + relative.remove_prefix(separator + 1); + } +} + +std::filesystem::path ExecOwnExecutable() { +#if defined(_WIN32) + std::wstring own(32768, L'\0'); + const DWORD length = GetModuleFileNameW(nullptr, own.data(), static_cast(own.size())); + if (length == 0 || length >= own.size()) + throw std::system_error(GetLastError(), std::system_category(), "exec: GetModuleFileNameW"); + own.resize(length); + return std::filesystem::path(own); +#elif defined(__APPLE__) + std::uint32_t size = 0; + _NSGetExecutablePath(nullptr, &size); + std::vector executable(size); + std::error_code error; + if (_NSGetExecutablePath(executable.data(), &size) == 0) return std::filesystem::canonical(executable.data(), error); + throw std::runtime_error("exec: cannot locate the executable"); +#else + std::error_code error; + const auto own = std::filesystem::read_symlink("/proc/self/exe", error); + if (error) throw std::system_error(error, "exec: cannot read /proc/self/exe"); + return own; +#endif +} + +std::filesystem::path ExecConvertedTarget(const std::string& guest) { + const auto own = ExecOwnExecutable(); + if (guest == ExecMainExecutable) return own; + std::error_code error; + const auto name = std::filesystem::path(reinterpret_cast((ExecFlattenedName(guest) + own.extension().string()).c_str())); + const auto target = own.parent_path() / name; + if (!std::filesystem::is_regular_file(target, error)) + NotImplemented_nid_no_patch((std::string("exec: ") + guest + " without its converted executable; relink with --executable " + + std::string(guest.substr(ExecApplicationRoot.size()))) + .c_str()); + return target; +} + +std::size_t ExecEntriesBytes(char* const* entries) { + std::size_t bytes = 0; + for (std::size_t index = 0; entries[index] != nullptr; ++index) + bytes += std::strlen(entries[index]) + 1; + return bytes; +} + +std::vector ExecArgumentVector(char* const* arguments) { + std::vector values; + for (std::size_t index = 0; arguments[index] != nullptr; ++index) values.emplace_back(arguments[index]); + return values; +} + +#ifdef _WIN32 +std::wstring ExecWide(const std::string& text) { + if (text.empty()) return {}; + const int size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast(text.size()), nullptr, 0); + if (size <= 0) throw std::runtime_error("exec: argument is not valid UTF-8"); + std::wstring wide(static_cast(size), L'\0'); + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast(text.size()), wide.data(), size); + return wide; +} + +std::wstring ExecQuoted(const std::wstring& argument) { + if (!argument.empty() && argument.find_first_of(L" \t\n\v\"") == std::wstring::npos) return argument; + std::wstring quoted = L"\""; + for (std::size_t index = 0;; ++index) { + std::size_t backslashes = 0; + while (index < argument.size() && argument[index] == L'\\') { + ++backslashes; + ++index; + } + if (index == argument.size()) { + quoted.append(backslashes * 2, L'\\'); + break; + } + quoted.append(argument[index] == L'"' ? backslashes * 2 + 1 : backslashes, L'\\'); + quoted.push_back(argument[index]); + } + quoted.push_back(L'"'); + return quoted; +} + +void ExecDuplicate(HANDLE source, DWORD access, HANDLE* target) { + if (!DuplicateHandle(GetCurrentProcess(), source, GetCurrentProcess(), target, access, TRUE, access ? 0 : DUPLICATE_SAME_ACCESS)) + throw std::system_error(GetLastError(), std::system_category(), "exec: duplicating a handle"); +} + +void ExecCloseHandle(HANDLE value) { CloseHandle(value); } + +bool ExecWindowsImage(const std::filesystem::path& target) { + std::ifstream image(target, std::ios::binary); + std::array dos{}; + if (!image.read(dos.data(), dos.size()) || dos[0] != 'M' || dos[1] != 'Z') return false; + const auto header = static_cast(static_cast(dos[63])) << 24 | + static_cast(static_cast(dos[62])) << 16 | + static_cast(static_cast(dos[61])) << 8 | + static_cast(static_cast(dos[60])); + if (header < dos.size()) return false; + image.seekg(static_cast(header)); + std::array coff{}; + if (!image.read(coff.data(), coff.size())) return false; + return coff[0] == 'P' && coff[1] == 'E' && coff[2] == '\0' && coff[3] == '\0' && + static_cast(coff[4]) == 0x64 && static_cast(coff[5]) == 0x86; +} + +[[noreturn]] void ExecReplaceWindows(const std::filesystem::path& target, const std::vector& arguments, bool implicitEnvironment, + char* const* environment) { + constexpr wchar_t ParentVariable[] = L"ANYPS5_LOADEXEC_PARENT="; + constexpr wchar_t SkipVariable[] = L"ANYPS5_LOADEXEC_ARGV_SKIP="; + const wchar_t* parentPrefix = ParentVariable; + const wchar_t* skipPrefix = SkipVariable; + const std::size_t parentPrefixLength = std::size(ParentVariable) - 1; + const std::size_t skipPrefixLength = std::size(SkipVariable) - 1; + std::wstring command = ExecQuoted(target.wstring()); + for (const auto& argument : arguments) { + if (!command.empty()) command += L' '; + command += ExecQuoted(ExecWide(argument)); + } + if (command.size() >= 32767) throw std::runtime_error("exec: command line is too long"); + std::vector entries; + const std::unique_ptr inherited(GetEnvironmentStringsW(), FreeEnvironmentStringsW); + if (!inherited) throw std::runtime_error("exec: cannot read the process environment"); + for (const wchar_t* at = inherited.get(); *at;) { + const std::wstring entry(at); + at += entry.size() + 1; + entries.push_back(std::move(entry)); + } + if (!implicitEnvironment) { + const auto name = [](const std::wstring& entry) { + const std::size_t separator = entry.find(L'='); + return separator == std::wstring::npos ? entry : entry.substr(0, separator); + }; + for (std::size_t index = 0; environment[index] != nullptr; ++index) { + std::wstring replaced = ExecWide(environment[index]); + const std::wstring replacedName = name(replaced); + for (std::size_t existing = 0; existing < entries.size();) { + if (name(entries[existing]) == replacedName) { + entries.erase(entries.begin() + existing); + continue; + } + ++existing; + } + entries.push_back(std::move(replaced)); + } + } + entries.erase(std::remove_if(entries.begin(), entries.end(), + [parentPrefix, parentPrefixLength, skipPrefix, skipPrefixLength](const std::wstring& entry) { + return _wcsnicmp(entry.c_str(), parentPrefix, parentPrefixLength) == 0 || + _wcsnicmp(entry.c_str(), skipPrefix, skipPrefixLength) == 0; + }), + entries.end()); + std::unique_ptr parent(nullptr, ExecCloseHandle); + { + HANDLE shared = nullptr; + ExecDuplicate(GetCurrentProcess(), SYNCHRONIZE, &shared); + parent.reset(shared); + } + std::vector inheritedHandles{parent.get()}; + std::vector> standard; + STARTUPINFOEXW startup{}; + startup.StartupInfo.cb = sizeof(startup); + startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES; + HANDLE* outputs[] = {&startup.StartupInfo.hStdInput, &startup.StartupInfo.hStdOutput, &startup.StartupInfo.hStdError}; + const DWORD identifiers[] = {STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE}; + for (std::size_t index = 0; index < 3; ++index) { + const HANDLE source = GetStdHandle(identifiers[index]); + if (source && source != INVALID_HANDLE_VALUE) { + HANDLE handle = nullptr; + ExecDuplicate(source, 0, &handle); + standard.emplace_back(handle, ExecCloseHandle); + inheritedHandles.push_back(handle); + *outputs[index] = handle; + } + } + SIZE_T bytes = 0; + InitializeProcThreadAttributeList(nullptr, 1, 0, &bytes); + std::vector storage((bytes + sizeof(std::max_align_t) - 1) / sizeof(std::max_align_t)); + auto* attributes = reinterpret_cast(storage.data()); + if (!InitializeProcThreadAttributeList(attributes, 1, 0, &bytes)) + throw std::system_error(GetLastError(), std::system_category(), "exec: creating process attributes"); + const std::unique_ptr<_PROC_THREAD_ATTRIBUTE_LIST, void (*)(LPPROC_THREAD_ATTRIBUTE_LIST)> cleanup(attributes, DeleteProcThreadAttributeList); + if (!UpdateProcThreadAttribute(attributes, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, inheritedHandles.data(), + inheritedHandles.size() * sizeof(HANDLE), nullptr, nullptr)) + throw std::system_error(GetLastError(), std::system_category(), "exec: selecting inherited handles"); + startup.lpAttributeList = attributes; + std::wostringstream value; + value << ParentVariable << std::hex << reinterpret_cast(parent.get()); + entries.push_back(value.str()); + entries.push_back(std::wstring(SkipVariable) + L"1"); + std::sort(entries.begin(), entries.end(), [](const auto& left, const auto& right) { return _wcsicmp(left.c_str(), right.c_str()) < 0; }); + std::vector block; + for (const auto& entry : entries) { + block.insert(block.end(), entry.begin(), entry.end()); + block.push_back(0); + } + block.push_back(0); + PROCESS_INFORMATION process{}; + if (!CreateProcessW(target.c_str(), command.data(), nullptr, nullptr, TRUE, + CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT, block.data(), nullptr, + &startup.StartupInfo, &process)) + throw std::system_error(GetLastError(), std::system_category(), "exec: creating the converted executable"); + const std::unique_ptr child(process.hProcess, ExecCloseHandle); + const std::unique_ptr thread(process.hThread, ExecCloseHandle); + try { + if (ResumeThread(process.hThread) == static_cast(-1)) + throw std::system_error(GetLastError(), std::system_category(), "exec: starting the converted executable"); + TerminateProcess(GetCurrentProcess(), 0); + } catch (...) { + TerminateProcess(process.hProcess, 1); + throw; + } + std::abort(); +} +#endif +} + extern "C" { // unknown data @@ -424,10 +720,58 @@ int APS5_VABI system_nid_postfix(const char* command) { } int APS5_VABI waitpid_nid_postfix(int pid, int* status, int options) { - (void)pid; - (void)status; - *__error_nid_postfix() = (static_cast(options) & ~freebsdWaitOptions) != 0 ? errnoInvalidArgument : errnoNoChild; - return -1; + if ((static_cast(options) & ~freebsdWaitOptions) != 0) { + *__error_nid_postfix() = errnoInvalidArgument; + return -1; + } +#ifdef _WIN32 + int encoded = 0; + const std::int64_t reaped = WaitChildProcess_nid_no_patch(pid, static_cast(options), &encoded); + if (reaped == -2) { + *__error_nid_postfix() = errnoNoChild; + return -1; + } + if (reaped == -1) return -1; + if (reaped == 0) return 0; + if (status != nullptr) *status = encoded; + return static_cast(reaped); +#else + unsigned hostOptions = static_cast(options) & 3u; + if ((static_cast(options) & 4u) != 0) hostOptions |= static_cast(WCONTINUED); + int encoded = 0; + const int result = ::waitpid(pid, &encoded, static_cast(hostOptions)); + if (result < 0) { + *__error_nid_postfix() = errno; + return -1; + } + if (result == 0) return 0; + if (WIFCONTINUED(encoded)) { + encoded = 0x13; + } else if (WIFSIGNALED(encoded) || WIFSTOPPED(encoded)) { + const int hostSignal = WIFSIGNALED(encoded) ? WTERMSIG(encoded) : WSTOPSIG(encoded); + int guestSignal = hostSignal; + switch (hostSignal) { + case 7: guestSignal = 10; break; + case 10: guestSignal = 30; break; + case 12: guestSignal = 31; break; + case 17: guestSignal = 20; break; + case 18: guestSignal = 19; break; + case 19: guestSignal = 17; break; + case 20: guestSignal = 18; break; + case 23: guestSignal = 16; break; + case 29: guestSignal = 23; break; + case 31: guestSignal = 12; break; + case 16: + case 30: + NotImplemented_nid_no_patch("waitpid: a child stopped or died with a signal without a guest equivalent"); + return -1; + default: break; + } + encoded = WIFSTOPPED(encoded) ? ((encoded & ~0xff00) | (guestSignal << 8)) : ((encoded & ~0x7f) | guestSignal); + } + if (status != nullptr) *status = encoded; + return result; +#endif } int APS5_VABI setrlimit_nid_postfix(int resource, const GuestResourceLimit* limit) { @@ -474,26 +818,77 @@ int APS5_VABI wait_nid_postfix(int* status) { return waitpid_nid_postfix(-1, status, 0); } -int APS5_VABI execvp_nid_postfix(const char* file, char* const* arguments) { - (void)file; - (void)arguments; - NotImplemented_nid_no_patch("execvp: executable replacement"); +int ExecProgram(const char* file, char* const* arguments, char* const* environment, bool search, bool implicitEnvironment) { + const auto fail = [](int error) { *__error_nid_postfix() = error; return -1; }; + if (file == nullptr) return fail(errnoBadFile); + if (arguments == nullptr) return fail(errnoBadAddress); + if (file[0] == '\0') return fail(errnoNoEnt); + static char* const noEnvironment[] = {nullptr}; + char* const* entries = implicitEnvironment ? static_cast(environ) : (environment != nullptr ? environment : noEnvironment); + if (ExecEntriesBytes(arguments) + ExecEntriesBytes(entries) > ExecArgumentBytesLimit) return fail(errnoTooBig); + int error = errnoNoEnt; + std::string program; + if (!ExecProgramFound(file, search, error, program)) return fail(error); + const auto target = ExecConvertedTarget(program); +#ifdef _WIN32 + if (!ExecWindowsImage(target)) return fail(errnoNoExec); + ExecReplaceWindows(target, ExecArgumentVector(arguments), implicitEnvironment, entries); +#elif defined(__linux__) + const int descriptor = ::open(target.c_str(), O_RDONLY | O_CLOEXEC); + if (descriptor < 0) throw std::system_error(errno, std::generic_category(), "exec: opening the converted executable"); + std::array header{}; + if (::pread(descriptor, header.data(), header.size(), 0) != static_cast(header.size()) || header[0] != 0x7f || + header[1] != 'E' || header[2] != 'L' || header[3] != 'F' || header[4] != 2 || header[5] != 1 || + (header[7] != 0 && header[7] != 3) || header[17] != 0 || (header[16] != 2 && header[16] != 3) || header[18] != 62 || + header[19] != 0) { + ::close(descriptor); + return fail(errnoNoExec); + } + if (::access(target.c_str(), X_OK) != 0) { + ::close(descriptor); + return fail(errnoAccess); + } + std::vector merged; + if (!implicitEnvironment) + for (std::size_t index = 0; entries[index] != nullptr; ++index) merged.emplace_back(entries[index]); + for (std::size_t index = 0; environ[index] != nullptr; ++index) { + const std::string_view entry = environ[index]; + const std::size_t separator = entry.find('='); + const std::string_view name = separator == std::string_view::npos ? entry : entry.substr(0, separator); + bool overridden = false; + for (const auto& guest : merged) { + const std::size_t guestSeparator = guest.find('='); + if ((guestSeparator == std::string::npos ? std::string_view(guest) : std::string_view(guest).substr(0, guestSeparator)) == name) { + overridden = true; + break; + } + } + if (!overridden) merged.emplace_back(environ[index]); + } + std::vector replacement; + for (auto& entry : merged) replacement.push_back(entry.data()); + replacement.push_back(nullptr); + fexecve(descriptor, arguments, replacement.data()); + const int replaced = errno; + ::close(descriptor); + throw std::system_error(replaced, std::generic_category(), "exec: replacing the executable"); +#else + static_cast(target); + NotImplemented_nid_no_patch("exec: process replacement on this host"); return -1; +#endif +} + +int APS5_VABI execvp_nid_postfix(const char* file, char* const* arguments) { + return ExecProgram(file, arguments, nullptr, true, true); } int APS5_VABI execv_nid_postfix(const char* path, char* const* arguments) { - (void)path; - (void)arguments; - NotImplemented_nid_no_patch("execv: executable replacement"); - return -1; + return ExecProgram(path, arguments, nullptr, false, true); } int APS5_VABI execve_nid_postfix(const char* path, char* const* arguments, char* const* environment) { - (void)path; - (void)arguments; - (void)environment; - NotImplemented_nid_no_patch("execve: executable replacement"); - return -1; + return ExecProgram(path, arguments, environment, false, false); } int APS5_VABI sceKernelGetCurrentCpu(void) { @@ -644,8 +1039,12 @@ int APS5_VABI getrusage_nid_postfix(int who, GuestResourceUsage* usage) { *__error_nid_postfix() = guestFault; return -1; } + GuestResourceUsage result{}; if (who == -1) { - *usage = {}; + if (!WriteGuest(usage, &result, sizeof(result))) { + *__error_nid_postfix() = guestFault; + return -1; + } return 0; } #ifdef _WIN32 @@ -665,47 +1064,37 @@ int APS5_VABI getrusage_nid_postfix(int who, GuestResourceUsage* usage) { }; const auto userMicros = toMicros(user); const auto kernelMicros = toMicros(kernel); - usage->ru_utime.tv_sec = static_cast(userMicros / 1000000ULL); - usage->ru_utime.tv_usec = static_cast(userMicros % 1000000ULL); - usage->ru_stime.tv_sec = static_cast(kernelMicros / 1000000ULL); - usage->ru_stime.tv_usec = static_cast(kernelMicros % 1000000ULL); - usage->ru_maxrss = 0; - usage->ru_ixrss = 0; - usage->ru_idrss = 0; - usage->ru_isrss = 0; - usage->ru_minflt = 0; - usage->ru_majflt = 0; - usage->ru_nswap = 0; - usage->ru_inblock = 0; - usage->ru_oublock = 0; - usage->ru_msgsnd = 0; - usage->ru_msgrcv = 0; - usage->ru_nsignals = 0; - usage->ru_nvcsw = 0; - usage->ru_nivcsw = 0; + result.ru_utime.tv_sec = static_cast(userMicros / 1000000ULL); + result.ru_utime.tv_usec = static_cast(userMicros % 1000000ULL); + result.ru_stime.tv_sec = static_cast(kernelMicros / 1000000ULL); + result.ru_stime.tv_usec = static_cast(kernelMicros % 1000000ULL); #else struct rusage native{}; if (::getrusage(who == 0 ? RUSAGE_SELF : RUSAGE_THREAD, &native) != 0) throw std::system_error(errno, std::generic_category(), "getrusage: getrusage failed"); - usage->ru_utime.tv_sec = static_cast(native.ru_utime.tv_sec); - usage->ru_utime.tv_usec = static_cast(native.ru_utime.tv_usec); - usage->ru_stime.tv_sec = static_cast(native.ru_stime.tv_sec); - usage->ru_stime.tv_usec = static_cast(native.ru_stime.tv_usec); - usage->ru_maxrss = static_cast(native.ru_maxrss); - usage->ru_ixrss = static_cast(native.ru_ixrss); - usage->ru_idrss = static_cast(native.ru_idrss); - usage->ru_isrss = static_cast(native.ru_isrss); - usage->ru_minflt = static_cast(native.ru_minflt); - usage->ru_majflt = static_cast(native.ru_majflt); - usage->ru_nswap = static_cast(native.ru_nswap); - usage->ru_inblock = static_cast(native.ru_inblock); - usage->ru_oublock = static_cast(native.ru_oublock); - usage->ru_msgsnd = static_cast(native.ru_msgsnd); - usage->ru_msgrcv = static_cast(native.ru_msgrcv); - usage->ru_nsignals = static_cast(native.ru_nsignals); - usage->ru_nvcsw = static_cast(native.ru_nvcsw); - usage->ru_nivcsw = static_cast(native.ru_nivcsw); + result.ru_utime.tv_sec = static_cast(native.ru_utime.tv_sec); + result.ru_utime.tv_usec = static_cast(native.ru_utime.tv_usec); + result.ru_stime.tv_sec = static_cast(native.ru_stime.tv_sec); + result.ru_stime.tv_usec = static_cast(native.ru_stime.tv_usec); + result.ru_maxrss = static_cast(native.ru_maxrss); + result.ru_ixrss = static_cast(native.ru_ixrss); + result.ru_idrss = static_cast(native.ru_idrss); + result.ru_isrss = static_cast(native.ru_isrss); + result.ru_minflt = static_cast(native.ru_minflt); + result.ru_majflt = static_cast(native.ru_majflt); + result.ru_nswap = static_cast(native.ru_nswap); + result.ru_inblock = static_cast(native.ru_inblock); + result.ru_oublock = static_cast(native.ru_oublock); + result.ru_msgsnd = static_cast(native.ru_msgsnd); + result.ru_msgrcv = static_cast(native.ru_msgrcv); + result.ru_nsignals = static_cast(native.ru_nsignals); + result.ru_nvcsw = static_cast(native.ru_nvcsw); + result.ru_nivcsw = static_cast(native.ru_nivcsw); #endif + if (!WriteGuest(usage, &result, sizeof(result))) { + *__error_nid_postfix() = guestFault; + return -1; + } return 0; } diff --git a/core/libs/prx/libkernel/System/src/Rtld.cpp b/core/libs/prx/libkernel/System/src/Rtld.cpp index 5d424ba846..cbce3c4e09 100644 --- a/core/libs/prx/libkernel/System/src/Rtld.cpp +++ b/core/libs/prx/libkernel/System/src/Rtld.cpp @@ -36,15 +36,13 @@ const void* imageContaining(const void* address, bool reference, void*& handle, #ifdef _WIN32 const DWORD flags = GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | (reference ? 0 : GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT); HMODULE module = nullptr; - if (!GetModuleHandleExW(flags, reinterpret_cast(address), &module)) - throw std::runtime_error(std::string(caller) + ": address outside every loaded image"); + if (!GetModuleHandleExW(flags, reinterpret_cast(address), &module)) return nullptr; if (reference) handle = module; return module; #else Dl_info info{}; link_map* image = nullptr; - if (!dladdr1(address, &info, reinterpret_cast(&image), RTLD_DL_LINKMAP) || !image) - throw std::runtime_error(std::string(caller) + ": address outside every loaded image"); + if (!dladdr1(address, &info, reinterpret_cast(&image), RTLD_DL_LINKMAP) || !image) return nullptr; if (reference && image->l_name && *image->l_name) { handle = dlopen(image->l_name, RTLD_LAZY | RTLD_NOLOAD); if (!handle) throw std::runtime_error(std::string(caller) + ": cannot reference " + image->l_name); @@ -56,6 +54,7 @@ const void* imageContaining(const void* address, bool reference, void*& handle, int referenceThreadAtexitImage(const void* address, const char* caller) { void* handle = nullptr; const void* image = imageContaining(address, true, handle, caller); + if (!image) return -1; std::lock_guard lock(threadAtexitMutex); threadAtexitReferences.emplace(image, handle); return 0; @@ -64,6 +63,7 @@ int referenceThreadAtexitImage(const void* address, const char* caller) { int releaseThreadAtexitImage(const void* address, const char* caller) { void* handle = nullptr; const void* image = imageContaining(address, false, handle, caller); + if (!image) return 0; { std::lock_guard lock(threadAtexitMutex); const auto found = threadAtexitReferences.find(image); diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index cc99d90231..80cb2e88d3 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -2,8 +2,10 @@ #include "prx/libc/include/Api.hpp" #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/JumpBuffer.hpp" #include "prx/libkernel/Pthread/Posix/Common.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -32,12 +34,20 @@ constexpr std::uint32_t UnblockableSignals = (1u << (GuestSigkill - 1)) | (1u << constexpr int GuestSigalrm = 14; constexpr int GuestSigcont = 19; constexpr int SaRestart = 0x2; +constexpr int SaResethand = 0x4; +constexpr int SaNodefer = 0x10; constexpr int SaSiginfo = 0x40; constexpr int SaDefinedFlags = 0x7f; std::atomic handlers[MaxSignal + 1]{}; +std::atomic handlerMasks[MaxSignal + 1][4]{}; +std::atomic handlerFlags[MaxSignal + 1]{}; +std::atomic handlerReset[MaxSignal + 1]{}; static_assert(std::atomic::is_always_lock_free); std::atomic blockedMask{0}; std::atomic blockedMaskExtra[3]{}; +std::atomic handlerHolds[MaxSignal + 1]{}; +constexpr std::uint64_t HoldCount = 0xffffffffu; +thread_local std::uint32_t holdReleases[MaxSignal + 1]{}; static_assert(std::atomic::is_always_lock_free); std::mutex registration; GuestSigaction dispositions[MaxSignal + 1]{}; @@ -73,16 +83,34 @@ std::uint32_t SignalBit(int guest) { return 1u << ((guest - 1) % 32); } std::uint32_t SignalWord(int guest) { return static_cast((guest - 1) / 32); } bool SignalInSet(const GuestSignalSet& set, int guest) { return (set.bits[SignalWord(guest)] & SignalBit(guest)) != 0; } bool SetHasSignal(const GuestSignalSet& set) { return (set.bits[0] | set.bits[1] | set.bits[2] | set.bits[3]) != 0; } +bool HeldByHandler(int guest) { return (handlerHolds[guest].load() & HoldCount) != 0; } +void ReleaseHold(int guest, std::uint32_t epoch) { + auto value = handlerHolds[guest].load(); + while ((value >> 32) == epoch && (value & HoldCount) != 0 && !handlerHolds[guest].compare_exchange_weak(value, value - 1)) {} +} +void ClearHold(int guest) { + auto value = handlerHolds[guest].load(); + while ((value & HoldCount) != 0 && !handlerHolds[guest].compare_exchange_weak(value, ((value >> 32) + 1) << 32)) {} +} +std::uint32_t HeldWord(int word) { + std::uint32_t held = 0; + for (int bit = 0; bit < 32; ++bit) + if (HeldByHandler(word * 32 + bit + 1)) held |= 1u << bit; + return held; +} +std::uint32_t BlockedWord(int word) { + return (word == 0 ? blockedMask.load() : blockedMaskExtra[word - 1].load()) | HeldWord(word); +} bool SignalBlocked(int guest) { if (guest < 1 || guest > MaxSignal) return false; const auto word = SignalWord(guest); const auto mask = word == 0 ? blockedMask.load() : blockedMaskExtra[word - 1].load(); - return (mask & SignalBit(guest)) != 0; + return (mask & SignalBit(guest)) != 0 || HeldByHandler(guest); } struct PendingSignals { std::mutex lock; - std::condition_variable changed; + TimedWait::Condition changed; std::atomic words[4]{}; }; @@ -99,7 +127,7 @@ void MarkPending(int guest) { std::lock_guard lock(pending.lock); pending.words[SignalWord(guest)].fetch_or(SignalBit(guest), std::memory_order_relaxed); } - pending.changed.notify_all(); + pending.changed.NotifyAll(); } bool TakePending(int guest) { @@ -142,6 +170,41 @@ int NativeSignal(int guest) { default: return 0; } } +void ResetToDefault(int guest) { + handlerReset[guest].store(true); + handlers[guest].store(nullptr); + if (const int native = NativeSignal(guest)) std::signal(native, SIG_DFL); +} +void RunHandler(int guest, GuestHandler handler) { + const int flags = handlerFlags[guest].load(); + std::uint32_t mask[4]; + for (int word = 0; word < 4; ++word) mask[word] = handlerMasks[guest][word].load(); + if ((flags & SaNodefer) == 0) mask[SignalWord(guest)] |= SignalBit(guest); + mask[0] &= ~UnblockableSignals; + if ((flags & SaResethand) != 0) ResetToDefault(guest); + std::uint32_t before[4]; + for (int word = 0; word < 4; ++word) before[word] = (word == 0 ? blockedMask.load() : blockedMaskExtra[word - 1].load()) & mask[word]; + std::uint32_t epochs[MaxSignal + 1]{}; + std::uint32_t releases[MaxSignal + 1]{}; + for (int held = 1; held <= MaxSignal; ++held) { + if ((mask[SignalWord(held)] & SignalBit(held)) == 0) continue; + releases[held] = holdReleases[held]; + epochs[held] = static_cast(handlerHolds[held].fetch_add(1) >> 32); + } + handler(guest); + std::uint32_t replaced[4]{}; + for (int held = 1; held <= MaxSignal; ++held) { + if ((mask[SignalWord(held)] & SignalBit(held)) == 0) continue; + if (holdReleases[held] != releases[held]) replaced[SignalWord(held)] |= SignalBit(held); + ReleaseHold(held, epochs[held]); + } + for (int word = 0; word < 4; ++word) { + if (replaced[word] == 0) continue; + auto& target = word == 0 ? blockedMask : blockedMaskExtra[word - 1]; + target.fetch_and(~(replaced[word] & ~before[word])); + target.fetch_or(replaced[word] & before[word]); + } +} void Dispatch(int native) { int guest = 0; for (int candidate : {2, 4, 6, 8, 11, 15}) @@ -156,12 +219,15 @@ void Dispatch(int native) { return; } const auto callback = handlers[guest].load(); - if (reinterpret_cast(callback) > 1) callback(guest); + if (reinterpret_cast(callback) > 1) RunHandler(guest, callback); } bool Delivered(const GuestSigaction& action) { return action.handler > 1 && (action.flags & SaSiginfo) == 0; } bool Install(int guest, const GuestSigaction& action) { + handlerReset[guest].store(false); + for (int word = 0; word < 4; ++word) handlerMasks[guest][word].store(action.mask.bits[word]); + handlerFlags[guest].store(action.flags); const int native = NativeSignal(guest); if (!native) return true; const auto previous = handlers[guest].exchange(Delivered(action) ? reinterpret_cast(action.handler) : nullptr); @@ -176,8 +242,15 @@ bool Install(int guest, const GuestSigaction& action) { namespace { +void SettleReset(int guest) { + if (!handlerReset[guest].exchange(false)) return; + dispositions[guest].handler = 0; + dispositions[guest].flags &= ~SaSiginfo; +} + GuestSigaction Disposition(int guest) { std::lock_guard lock(registration); + SettleReset(guest); return dispositions[guest]; } @@ -220,25 +293,39 @@ bool DeliverPendingSignal(int guest) { return action.handler > 1; } if (action.handler > 1) { - reinterpret_cast(action.handler)(guest); + RunHandler(guest, reinterpret_cast(action.handler)); return true; } return false; } +bool DeliverUnblockedPending() { + bool caught = false; + for (int guest = 1; guest <= MaxSignal; ++guest) { + if (SignalBlocked(guest) || !Deliverable(guest) || !TakePending(guest)) continue; + if (DeliverPendingSignal(guest)) caught = true; + } + return caught; +} + +std::optional SignalWaitDeadline(const KernelTimespec& timeout) { + if (timeout.tv_sec > INT32_MAX) return std::nullopt; + const std::uint64_t now = TimedWait::NowNanos(); + if (timeout.tv_sec < 0) return now; + return now + static_cast(timeout.tv_sec) * 1000000000ULL + static_cast(timeout.tv_nsec); +} + int WaitForPendingInSet(const GuestSignalSet& set, bool waitForever, const KernelTimespec* timeout, int* guest) { auto& pending = Pending(); std::unique_lock lock(pending.lock); - std::optional deadline; + const std::optional deadline = waitForever ? std::nullopt : SignalWaitDeadline(*timeout); for (;;) { if (PendingInSet(set, guest)) return 0; - if (waitForever) { - pending.changed.wait(lock); + if (!deadline) { + pending.changed.Wait(lock); continue; } - if (!deadline) deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout->tv_sec) + - std::chrono::nanoseconds(timeout->tv_nsec); - if (pending.changed.wait_until(lock, *deadline) == std::cv_status::no_timeout) continue; + if (pending.changed.WaitUntil(lock, *deadline)) continue; return PendingInSet(set, guest) ? 0 : GuestEagain; } } @@ -257,7 +344,7 @@ void WaitForDeliverableSignal() { { std::unique_lock lock(pending.lock); if (!TakePendingUnblocked(&guest)) { - pending.changed.wait(lock); + pending.changed.Wait(lock); continue; } } @@ -459,11 +546,61 @@ constexpr std::size_t MinSignalStackSize = 2048; thread_local GuestStack alternateStack{nullptr, 0, SsDisable}; } +namespace { + +int ChangeBlockedMask(int how, const GuestSignalSet* set, GuestSignalSet* previousSet, bool releaseHolds, bool* caught = nullptr) { + if (set != nullptr && (how < 1 || how > 3)) { *__error_nid_postfix() = 22; return -1; } + { + std::lock_guard lock(registration); + if (previousSet != nullptr) + for (int word = 0; word < 4; ++word) previousSet->bits[word] = BlockedWord(word); + if (set != nullptr) { + switch (how) { + case 1: + blockedMask.fetch_or(set->bits[0] & ~UnblockableSignals); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_or(set->bits[word]); + break; + case 2: + blockedMask.fetch_and(~set->bits[0]); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_and(~set->bits[word]); + if (releaseHolds) + for (int guest = 1; guest <= MaxSignal; ++guest) { + if (!SignalInSet(*set, guest)) continue; + ClearHold(guest); + ++holdReleases[guest]; + } + break; + case 3: + blockedMask.store(set->bits[0] & ~UnblockableSignals); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].store(set->bits[word]); + if (releaseHolds) + for (int guest = 1; guest <= MaxSignal; ++guest) { + ClearHold(guest); + ++holdReleases[guest]; + } + break; + } + } + } + std::atomic_thread_fence(std::memory_order_seq_cst); + if (DeliverUnblockedPending() && caught != nullptr) *caught = true; + return 0; +} + +} + +extern "C" int APS5_VABI SetSignalMaskCatching_nid_no_patch(const GuestSignalSet* set, GuestSignalSet* previousSet) { + bool caught = false; + ChangeBlockedMask(GuestSigsetmask, set, previousSet, true, &caught); + return caught ? 1 : 0; +} + extern "C" { GuestHandler APS5_VABI signal_nid_postfix(int guest, GuestHandler handler) { const auto invalid = reinterpret_cast(static_cast(-1)); if (guest < 1 || guest > MaxSignal || ((guest == GuestSigkill || guest == GuestSigstop) && handler != nullptr)) { *__error_nid_postfix() = 22; return invalid; } std::lock_guard lock(registration); + SettleReset(guest); const GuestSigaction action{reinterpret_cast(handler), SignalInSet(_sigintr_nid_postfix, guest) ? 0 : SaRestart, {}}; if (!Install(guest, action)) { *__error_nid_postfix() = 22; return invalid; } const auto previous = reinterpret_cast(dispositions[guest].handler); @@ -476,6 +613,7 @@ int APS5_VABI sigaction_nid_postfix(int guest, const GuestSigaction* action, Gue if (action && action->handler > 1 && (action->flags & ~SaDefinedFlags) != 0) { *__error_nid_postfix() = 22; return -1; } if (action && (guest == GuestSigkill || guest == GuestSigstop) && action->handler != 0) { *__error_nid_postfix() = 22; return -1; } std::lock_guard lock(registration); + SettleReset(guest); const GuestSigaction old = dispositions[guest]; if (action) { if (!Install(guest, *action)) { *__error_nid_postfix() = 22; return -1; } @@ -502,11 +640,13 @@ int APS5_VABI raise_nid_postfix(int guest) { const int native = NativeSignal(guest); if (native != 0) { const int result = std::raise(native); - if (result) *__error_nid_postfix() = 22; - return result ? -1 : 0; + if (result) { *__error_nid_postfix() = 22; return -1; } + DeliverUnblockedPending(); + return 0; } if (!CaughtByGuest(guest)) { *__error_nid_postfix() = 22; return -1; } DeliverPendingSignal(guest); + DeliverUnblockedPending(); return 0; } int APS5_VABI kill_nid_postfix(int pid, int guest) { @@ -541,36 +681,7 @@ int APS5_VABI sigaltstack_nid_postfix(const GuestStack* stack, GuestStack* previ return 0; } int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, GuestSignalSet* previousSet) { - if (set != nullptr && (how < 1 || how > 3)) { *__error_nid_postfix() = 22; return -1; } - { - std::lock_guard lock(registration); - if (previousSet != nullptr) { - previousSet->bits[0] = blockedMask.load(); - for (int word = 1; word < 4; ++word) previousSet->bits[word] = blockedMaskExtra[word - 1].load(); - } - if (set != nullptr) { - switch (how) { - case 1: - blockedMask.fetch_or(set->bits[0] & ~UnblockableSignals); - for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_or(set->bits[word]); - break; - case 2: - blockedMask.fetch_and(~set->bits[0]); - for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_and(~set->bits[word]); - break; - case 3: - blockedMask.store(set->bits[0] & ~UnblockableSignals); - for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].store(set->bits[word]); - break; - } - } - } - std::atomic_thread_fence(std::memory_order_seq_cst); - for (int guest = 1; guest <= MaxSignal; ++guest) { - if (SignalBlocked(guest) || !Deliverable(guest) || !TakePending(guest)) continue; - DeliverPendingSignal(guest); - } - return 0; + return ChangeBlockedMask(how, set, previousSet, true); } int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet) { @@ -628,7 +739,7 @@ int APS5_VABI sigsuspend_nid_postfix(const GuestSignalSet* set) { for (int word = 1; word < 4; ++word) previous.bits[word] = blockedMaskExtra[word - 1].exchange(set->bits[word]); } WaitForDeliverableSignal(); - _sigprocmask_nid_postfix(GuestSigsetmask, &previous, nullptr); + ChangeBlockedMask(GuestSigsetmask, &previous, nullptr, false); *__error_nid_postfix() = GuestEintr; return -1; @@ -642,9 +753,8 @@ int APS5_VABI sigpending_nid_postfix(GuestSignalSet* set) { if (set == nullptr) { *__error_nid_postfix() = 14; return -1; } auto& pending = Pending(); std::lock_guard lock(pending.lock); - set->bits[0] = pending.words[0].load(std::memory_order_relaxed) & blockedMask.load(); - for (int word = 1; word < 4; ++word) - set->bits[word] = pending.words[word].load(std::memory_order_relaxed) & blockedMaskExtra[word - 1].load(); + for (int word = 0; word < 4; ++word) + set->bits[word] = pending.words[word].load(std::memory_order_relaxed) & BlockedWord(word); return 0; } } @@ -659,6 +769,12 @@ int APS5_VABI _is_signal_return_nid_postfix(std::uint64_t programCounter) { } namespace { + +[[maybe_unused]] const bool RegisteredForJumps = [] { + JumpBuffer::GuestSigprocmaskRegister_nid_no_patch(sigprocmask_nid_postfix); + return true; +}(); + struct RegisterSignalsOnLoad { RegisterSignalsOnLoad() { RegisterSigaction_nid_no_patch(reinterpret_cast(sigaction_nid_postfix), _sigintr_nid_postfix.bits); diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index 7abef3192e..3a85190917 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -5,7 +5,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" -#include "prx/libkernel/System/include/HostCpu.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include "SceTypes.hpp" #include @@ -13,9 +12,11 @@ #include #include #include +#include #include #include #include +#include #include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN @@ -32,7 +33,7 @@ int CopyOut(const void* value, std::size_t size, void* oldValue, std::size_t* ol return 0; } const std::size_t copied = std::min(oldLength != nullptr ? *oldLength : 0, size); - std::memcpy(oldValue, value, copied); + if (copied != 0) std::memcpy(oldValue, value, copied); if (oldLength != nullptr) *oldLength = copied; if (copied < size) { *__error_nid_postfix() = 12; @@ -49,9 +50,10 @@ struct SystemString { constexpr SystemString SystemStrings[] = { {{1, 1}, "kern.ostype", "FreeBSD"}, - {{1, 2}, "kern.osrelease", "11.4-RELEASE"}, + {{1, 2}, "kern.osrelease", "0.0-prototype"}, {{1, 4}, "kern.version", "FreeBSD 11.4-RELEASE\n"}, - {{1, 10}, "kern.hostname", "AnyPS5"}, + {{1, 10}, "kern.hostname", ""}, + {{6, 2}, "hw.model", "100-000001020 "}, {{6, 1}, "hw.machine", "amd64"}, }; @@ -62,7 +64,10 @@ struct SystemInteger { }; constexpr SystemInteger SystemIntegers[] = { - {{1, 24}, "kern.osreldate", 1104001}, + {{1, 3}, "kern.osrevision", 199506}, + {{1, 8}, "kern.argmax", 262144}, + {{1, 18}, "kern.ngroups", 1023}, + {{1, 24}, "kern.osreldate", 900000}, }; const SystemString* FindSystemString(const int* name) { @@ -94,6 +99,21 @@ KernelTimeval BootTime() { return bootTime; } +constexpr std::size_t MaximumSysctlName = 1024; + +int TopLevelNode(std::string_view name) { + static constexpr std::string_view nodes[] = {"sysctl", "kern", "vm", "vfs", "net", "debug", "hw", "machdep", "user", "p1003_1b"}; + for (std::size_t i = 0; i < std::size(nodes); ++i) + if (name == nodes[i]) return static_cast(i); + return -1; +} + +std::string WithoutTrailingDot(const char* name) { + std::string_view view(name); + if (!view.empty() && view.back() == '.') view.remove_suffix(1); + return std::string(view); +} + bool IsMeasuredAbsentMib(const int* name, std::uint32_t nameLength) { static constexpr struct { int mib[3]; @@ -161,9 +181,13 @@ bool IsUnansweredName(const char* name) { static constexpr const char* unanswered[] = { "hw.byteorder", "hw.cacheline_size", + "hw.floatingpoint", "hw.machine_arch", "hw.usermem", + "kern.bootfile", "kern.clockrate", + "kern.cp_time", + "kern.ident", "kern.maxfiles", "kern.maxfilesperproc", "kern.maxproc", @@ -270,15 +294,6 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void const int value = static_cast(processors); return CopyOut(&value, sizeof(value), oldValue, oldLength); } - if (nameLength == 2 && name[0] == 6 && name[1] == 2) { - if (newValue != nullptr) { - *__error_nid_postfix() = 1; - return -1; - } - static const std::string model = HostCpuModel(); - if (model.empty()) throw std::runtime_error(std::string(__func__) + ": the host CPU has no CPUID brand string for hw.model"); - return CopyOut(model.c_str(), model.size() + 1, oldValue, oldLength); - } if (nameLength == 2 && name[0] == 6 && (name[1] == 5 || name[1] == 12)) { if (newValue != nullptr) { *__error_nid_postfix() = 1; @@ -389,6 +404,11 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void *__error_nid_postfix() = 20; return -1; } + if (name[0] < 0 || (name[0] >= 10 && name[0] < 256)) { + if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": writing MIB under absent top-level node " + std::to_string(name[0])); + *__error_nid_postfix() = 2; + return -1; + } std::string mib; for (std::uint32_t i = 0; i < nameLength; ++i) mib += (i == 0 ? "" : ".") + std::to_string(name[i]); throw std::runtime_error(std::string(__func__) + ": unsupported MIB " + mib); @@ -399,6 +419,16 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si *__error_nid_postfix() = 14; return -1; } + if (strnlen(name, MaximumSysctlName) == MaximumSysctlName) { + *__error_nid_postfix() = 63; + return -1; + } + const std::string trimmed = WithoutTrailingDot(name); + name = trimmed.c_str(); + if (TopLevelNode(trimmed) >= 0) { + *__error_nid_postfix() = 22; + return -1; + } if (std::strcmp(name, "machdep.cacheline_size") == 0) { *__error_nid_postfix() = 2; return -1; @@ -466,10 +496,6 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si static constexpr int mib[] = {6, 3}; return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); } - if (std::strcmp(name, "hw.model") == 0) { - static constexpr int mib[] = {6, 2}; - return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); - } if (std::strcmp(name, "hw.physmem") == 0) { static constexpr int mib[] = {6, 5}; return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); @@ -541,13 +567,14 @@ int APS5_VABI sysctlnametomib_nid_postfix(const char* name, int* mib, std::size_ {"kern.ps_strings", {1, 32}, 2}, {"kern.rng_pseudo", {1, 50}, 2}, }; - constexpr std::size_t MaximumPath = 1024; if (name == nullptr || size == nullptr) throw std::invalid_argument(std::string(__func__) + ": null name or size"); - const std::size_t nameLength = strnlen(name, MaximumPath); - if (nameLength == MaximumPath) { + const std::size_t nameLength = strnlen(name, MaximumSysctlName); + if (nameLength == MaximumSysctlName) { *__error_nid_postfix() = 63; return -1; } + const std::string trimmed = WithoutTrailingDot(name); + name = trimmed.c_str(); if (nameLength == 0 || std::strcmp(name, "hw.cacheline_size") == 0 || std::strcmp(name, "hw.usermem") == 0 || std::strcmp(name, "machdep.cacheline_size") == 0) { *__error_nid_postfix() = 2; return -1; @@ -558,6 +585,7 @@ int APS5_VABI sysctlnametomib_nid_postfix(const char* name, int* mib, std::size_ *size = bytes / sizeof(int); return result; }; + if (const int node = TopLevelNode(trimmed); node >= 0) return copyMib(&node, 1); for (const auto& entry : known) { if (std::strcmp(name, entry.name) == 0) return copyMib(entry.mib, entry.length); } diff --git a/core/libs/prx/libkernel/System/src/ThermalSensors.cpp b/core/libs/prx/libkernel/System/src/ThermalSensors.cpp index fcff781e17..6339926d84 100644 --- a/core/libs/prx/libkernel/System/src/ThermalSensors.cpp +++ b/core/libs/prx/libkernel/System/src/ThermalSensors.cpp @@ -1,4 +1,5 @@ #include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/Api.hpp" #include "prx/libkernel/System/include/ThermalSensors.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libc/include/general/VabiMacros.hpp" @@ -239,6 +240,11 @@ int ReadStorageTemperature() { #endif +int Fail(int error) { + *__error_nid_postfix() = error; + return SceKernelError(error); +} + void ReadHostSensors(int celsius[3]) { #ifdef _WIN32 celsius[2] = ReadStorageTemperature(); @@ -255,20 +261,21 @@ void ReadHostSensors(int celsius[3]) { extern "C" { int APS5_VABI sceKernelGetCpuTemperature(int* outCelsius) { - if (outCelsius == nullptr) return SCE_KERNEL_ERROR_EFAULT; + if (outCelsius == nullptr) return Fail(14); int celsius[3]; ReadHostSensors(celsius); - if (celsius[0] < 0) return SCE_KERNEL_ERROR_EINVAL; + if (celsius[0] < 0) return Fail(22); *outCelsius = celsius[0]; return 0; } int APS5_VABI sceKernelGetSocSensorTemperature(int sensorId, int* outCelsius) { - if (outCelsius == nullptr) return SCE_KERNEL_ERROR_EFAULT; - if (sensorId < 0 || sensorId >= 3) return SCE_KERNEL_ERROR_EINVAL; + if (sensorId < 0 || sensorId > 64) return Fail(22); + if (outCelsius == nullptr) return Fail(14); + if (sensorId >= 3) throw std::runtime_error("sceKernelGetSocSensorTemperature: channels 3 to 64 have no host sensor"); int celsius[3]; ReadHostSensors(celsius); - if (celsius[sensorId] < 0) return SCE_KERNEL_ERROR_EINVAL; + if (celsius[sensorId] < 0) return Fail(22); *outCelsius = celsius[sensorId]; return 0; } diff --git a/core/libs/prx/libkernel/System/src/UserContext.cpp b/core/libs/prx/libkernel/System/src/UserContext.cpp index 0ebbc448f2..0d3d7f5937 100644 --- a/core/libs/prx/libkernel/System/src/UserContext.cpp +++ b/core/libs/prx/libkernel/System/src/UserContext.cpp @@ -70,47 +70,58 @@ std::uint32_t HostMxcsrMask() { extern "C" [[noreturn]] void APS5_VABI Aps5ContextRestore_nid_no_patch(const RegisterFrame* frame); +#define APS5_CONTEXT_CAPTURE \ + " pushfq\n" \ + " push %rbp\n" \ + " mov %rsp, %rbp\n" \ + " sub $0x2a0, %rsp\n" \ + " and $-16, %rsp\n" \ + " mov %rax, 0x00(%rsp)\n" \ + " mov %rbx, 0x08(%rsp)\n" \ + " mov %rcx, 0x10(%rsp)\n" \ + " mov %rdx, 0x18(%rsp)\n" \ + " mov %rsi, 0x20(%rsp)\n" \ + " mov %rdi, 0x28(%rsp)\n" \ + " mov 0(%rbp), %rax\n" \ + " mov %rax, 0x30(%rsp)\n" \ + " lea 24(%rbp), %rax\n" \ + " mov %rax, 0x38(%rsp)\n" \ + " mov %r8, 0x40(%rsp)\n" \ + " mov %r9, 0x48(%rsp)\n" \ + " mov %r10, 0x50(%rsp)\n" \ + " mov %r11, 0x58(%rsp)\n" \ + " mov %r12, 0x60(%rsp)\n" \ + " mov %r13, 0x68(%rsp)\n" \ + " mov %r14, 0x70(%rsp)\n" \ + " mov %r15, 0x78(%rsp)\n" \ + " mov 16(%rbp), %rax\n" \ + " mov %rax, 0x80(%rsp)\n" \ + " mov 8(%rbp), %rax\n" \ + " mov %rax, 0x88(%rsp)\n" \ + " xor %eax, %eax\n" \ + " mov %cs, %ax\n" \ + " mov %rax, 0x90(%rsp)\n" \ + " mov %ss, %ax\n" \ + " mov %rax, 0x98(%rsp)\n" \ + " fxsave64 0xa0(%rsp)\n" + +#define APS5_CONTEXT_RETURN \ + " mov %rbp, %rsp\n" \ + " pop %rbp\n" \ + " lea 8(%rsp), %rsp\n" \ + " ret\n" + asm(".text\n" APS5_ASM_FUNCTION("getcontext_nid_postfix") - " pushfq\n" - " push %rbp\n" - " mov %rsp, %rbp\n" - " sub $0x2a0, %rsp\n" - " and $-16, %rsp\n" - " mov %rax, 0x00(%rsp)\n" - " mov %rbx, 0x08(%rsp)\n" - " mov %rcx, 0x10(%rsp)\n" - " mov %rdx, 0x18(%rsp)\n" - " mov %rsi, 0x20(%rsp)\n" - " mov %rdi, 0x28(%rsp)\n" - " mov 0(%rbp), %rax\n" - " mov %rax, 0x30(%rsp)\n" - " lea 24(%rbp), %rax\n" - " mov %rax, 0x38(%rsp)\n" - " mov %r8, 0x40(%rsp)\n" - " mov %r9, 0x48(%rsp)\n" - " mov %r10, 0x50(%rsp)\n" - " mov %r11, 0x58(%rsp)\n" - " mov %r12, 0x60(%rsp)\n" - " mov %r13, 0x68(%rsp)\n" - " mov %r14, 0x70(%rsp)\n" - " mov %r15, 0x78(%rsp)\n" - " mov 16(%rbp), %rax\n" - " mov %rax, 0x80(%rsp)\n" - " mov 8(%rbp), %rax\n" - " mov %rax, 0x88(%rsp)\n" - " xor %eax, %eax\n" - " mov %cs, %ax\n" - " mov %rax, 0x90(%rsp)\n" - " mov %ss, %ax\n" - " mov %rax, 0x98(%rsp)\n" - " fxsave64 0xa0(%rsp)\n" + APS5_CONTEXT_CAPTURE " mov %rsp, %rsi\n" " call " APS5_ASM_SYMBOL("Aps5GetcontextFinish_nid_no_patch") "\n" - " mov %rbp, %rsp\n" - " pop %rbp\n" - " lea 8(%rsp), %rsp\n" - " ret\n" + APS5_CONTEXT_RETURN + APS5_ASM_FUNCTION("swapcontext_nid_postfix") + APS5_CONTEXT_CAPTURE + " mov %rsp, %rdx\n" + " call " APS5_ASM_SYMBOL("Aps5SwapcontextFinish_nid_no_patch") "\n" + APS5_CONTEXT_RETURN APS5_ASM_FUNCTION("Aps5ContextRestore_nid_no_patch") " mov %rdi, %r12\n" " xor %eax, %eax\n" @@ -217,4 +228,10 @@ int APS5_VABI setcontext_nid_postfix(const UserUcontext* context) { Aps5ContextRestore_nid_no_patch(&frame); } +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); + return setcontext_nid_postfix(next); +} + } diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 4e3d97ebca..852bb34a1c 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -37,6 +37,8 @@ #ifdef __linux__ #include #include +#include +#include #endif // __linux__ #endif @@ -44,14 +46,10 @@ static constexpr int GUEST_EFAULT = 14; static constexpr int GUEST_EINVAL = 22; static constexpr int GUEST_CLOCK_REALTIME = 0; static constexpr int GUEST_CLOCK_THREAD_CPUTIME_ID = 14; +static constexpr int GUEST_CLOCK_PROCESS_CPUTIME_ID = 15; static bool IsCpuClock(int clockId) { - const auto bits = static_cast(clockId); - if ((bits & CPU_CLOCK_BIT) == 0) - return false; - if ((bits & CPU_CLOCK_PROCESS_BIT) != 0) - throw std::runtime_error("clock: unsupported process CPU clock_id " + std::to_string(clockId)); - return true; + return (static_cast(clockId) & CPU_CLOCK_BIT) != 0; } static std::uint64_t RawMonotonicNanos() { @@ -163,6 +161,45 @@ static const bool g_timerSelfTest = [] { }(); #endif +namespace { + +class CurrentCpuPin { +public: + CurrentCpuPin() { +#ifdef _WIN32 + previous = SetThreadAffinityMask(GetCurrentThread(), DWORD_PTR{1} << GetCurrentProcessorNumber()); +#elif defined(__linux__) + const int cpu = sched_getcpu(); + if (cpu < 0 || cpu >= CPU_SETSIZE || pthread_getaffinity_np(pthread_self(), sizeof(previous), &previous) != 0) return; + cpu_set_t one; + CPU_ZERO(&one); + CPU_SET(cpu, &one); + pinned = pthread_setaffinity_np(pthread_self(), sizeof(one), &one) == 0; +#endif + } + + ~CurrentCpuPin() { +#ifdef _WIN32 + if (previous != 0) SetThreadAffinityMask(GetCurrentThread(), previous); +#elif defined(__linux__) + if (pinned) pthread_setaffinity_np(pthread_self(), sizeof(previous), &previous); +#endif + } + + CurrentCpuPin(const CurrentCpuPin&) = delete; + CurrentCpuPin& operator=(const CurrentCpuPin&) = delete; + +private: +#ifdef _WIN32 + DWORD_PTR previous = 0; +#elif defined(__linux__) + cpu_set_t previous{}; + bool pinned = false; +#endif +}; + +} + extern "C" { std::uint64_t APS5_VABI sceKernelGetProcessTime() { @@ -391,8 +428,11 @@ static bool ProcessCpuResolutionNanos(std::uint64_t* nanos) { static int ReadClock(int clockId, KernelTimespec* tp) { if (IsCpuClock(clockId)) { + const int id = static_cast(static_cast(clockId) & CPU_CLOCK_ID_MASK); + const bool process = (static_cast(clockId) & CPU_CLOCK_PROCESS_BIT) != 0; + if (process && id == getpid_nid_postfix()) return ReadClock(GUEST_CLOCK_PROCESS_CPUTIME_ID, tp); std::uint64_t nanos = 0; - if (!GuestThreadCpuNanos(static_cast(static_cast(clockId) & CPU_CLOCK_ID_MASK), &nanos)) { + if (process || !GuestThreadCpuNanos(id, &nanos)) { *__error_nid_postfix() = GUEST_EINVAL; return -1; } @@ -501,6 +541,9 @@ int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp) { return 0; } +static std::mutex guestTimezoneMutex; +static KernelTimezone guestTimezone{0, 0}; + int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz) { KernelTimeval ignoredTime{}; if (tv == nullptr) tv = &ignoredTime; @@ -520,8 +563,8 @@ int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz) { tv->tv_usec = static_cast(native.tv_usec); #endif if (tz != nullptr) { - tz->tz_minuteswest = 0; - tz->tz_dsttime = 0; + std::lock_guard lock(guestTimezoneMutex); + *tz = guestTimezone; } return 0; } @@ -624,6 +667,23 @@ int APS5_VABI clock_settime_nid_postfix(int clockId, const KernelTimespec* tp) { throw std::runtime_error(std::string("clock_settime: setting CLOCK_REALTIME to ") + requested + " is not supported, the host clock is not changed"); } +int SetTimeOfDay_nid_no_patch(const KernelTimeval* tv, const KernelTimezone* tz) { + if (tv != nullptr && (tv->tv_usec < 0 || tv->tv_usec >= 1000000 || tv->tv_sec < 0)) { + *__error_nid_postfix() = GUEST_EINVAL; + return -1; + } + if (tv != nullptr) { + char requested[48]; + std::snprintf(requested, sizeof(requested), "%lld.%06lld", static_cast(tv->tv_sec), static_cast(tv->tv_usec)); + throw std::runtime_error(std::string("settimeofday: setting the time to ") + requested + " is not supported, the host clock is not changed"); + } + if (tz != nullptr) { + std::lock_guard lock(guestTimezoneMutex); + guestTimezone = *tz; + } + return 0; +} + // --------------------------------------------------------------------------- // Moved as-is (not yet implemented) from the monolithic libkernel/Export.cpp. // --------------------------------------------------------------------------- @@ -710,7 +770,8 @@ int APS5_VABI sceKernelGettimeofday(KernelTimeval* tp) { int APS5_VABI sceKernelGettimezone(KernelTimezone* tz) { if (tz == nullptr) return SceKernelErrorEfault; - *tz = {0, 0}; + const auto now = static_cast(std::time(nullptr)); + *tz = {static_cast(HostZoneAt(now).standardEast / 60), static_cast(HostDaylightAmount(now) / 60)}; return 0; } @@ -722,6 +783,7 @@ uint64_t APS5_VABI sceKernelReadTsc(void) { uint64_t APS5_VABI sceKernelGetTscFrequency(void) { static const std::uint64_t frequency = [] { + const CurrentCpuPin pin; const std::uint64_t startNanos = RawMonotonicNanos(); const std::uint64_t startTicks = __rdtsc(); TimedWait::SleepNanos(20000000ULL); diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index 87c6cb3406..20c5d80c94 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -6,11 +6,6 @@ extern "C" { -int APS5_VABI __tls_get_addr_nid_postfix(void) { - NotImplemented_nid_no_patch("vNe1w4diLCs"); - return 0; -} - int APS5_VABI physhm_open_nid_postfix(void) { NotImplemented_nid_no_patch("physhm_open"); return 0; diff --git a/core/libs/prx/libkernel/include/Api.hpp b/core/libs/prx/libkernel/include/Api.hpp index bbaac3dc55..77d0a5ec57 100644 --- a/core/libs/prx/libkernel/include/Api.hpp +++ b/core/libs/prx/libkernel/include/Api.hpp @@ -16,6 +16,11 @@ struct KernelIovec { std::size_t length; }; +struct KernelTlsIndex { + std::uint64_t module; + std::uint64_t offset; +}; + using GuestHandler = void (APS5_VABI *)(int); struct GuestItimerspec { @@ -143,7 +148,7 @@ int APS5_VABI sceKernelCancelEventFlag(KernelEventFlag ef, std::uint64_t setPatt extern "C" { -int APS5_VABI __tls_get_addr_nid_postfix(void); +void* APS5_VABI __tls_get_addr_nid_postfix(const KernelTlsIndex* index); int APS5_VABI physhm_open_nid_postfix(void); int APS5_VABI physhm_unlink_nid_postfix(void); int APS5_VABI sceKernelUnknown03(void); @@ -416,9 +421,11 @@ int APS5_VABI pthread_cond_signal_nid_postfix(PthreadCond* cond); int APS5_VABI pthread_cond_timedwait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, const KernelTimespec* abstime); int APS5_VABI pthread_cond_reltimedwait_np_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, KernelUseconds usec); int APS5_VABI pthread_cond_wait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex); +int APS5_VABI pthread_cond_setname_np_nid_postfix(PthreadCond* cond, const char* name); int APS5_VABI pthread_condattr_destroy_nid_postfix(PthreadCondattr* attr); int APS5_VABI pthread_condattr_init_nid_postfix(PthreadCondattr* attr); int APS5_VABI pthread_condattr_setclock_nid_postfix(PthreadCondattr* attr, KernelClockid clockId); +int APS5_VABI pthread_condattr_getclock_nid_postfix(const PthreadCondattr* attr, KernelClockid* clockId); int APS5_VABI pthread_condattr_setpshared_nid_postfix(PthreadCondattr* attr, int pshared); int APS5_VABI pthread_condattr_getpshared_nid_postfix(const PthreadCondattr* attr, int* pshared); int APS5_VABI pthread_mutex_destroy_nid_postfix(PthreadMutex* mutex); @@ -641,6 +648,8 @@ int APS5_VABI sceKernelRaiseException(Pthread thread, int signum); void APS5_VABI sceKernelDebugRaiseException(int c1, int c2); void APS5_VABI sceKernelDebugRaiseExceptionOnReleaseMode(int c1, int c2); int APS5_VABI getlogin_r_nid_postfix(char* output, int size); +char* APS5_VABI getlogin_nid_postfix(void); +int SetLogin_nid_no_patch(const char* name); int APS5_VABI sceKernelDebugOutText(int channel, const char* text); int APS5_VABI sceKernelSendNotificationRequest(int device, const void* request, std::size_t size, int flags); int APS5_VABI getargc_nid_postfix(void); @@ -723,6 +732,7 @@ int APS5_VABI killpg_nid_postfix(int pgrp, int guest); int APS5_VABI pthread_kill_nid_postfix(Pthread thread, int guest); int APS5_VABI sigaltstack_nid_postfix(const GuestStack* stack, GuestStack* previous); int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, GuestSignalSet* previousSet); +int APS5_VABI SetSignalMaskCatching_nid_no_patch(const GuestSignalSet* set, GuestSignalSet* previousSet); int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet); int APS5_VABI pthread_sigmask_nid_postfix(int how, const GuestSignalSet* set, GuestSignalSet* previousSet); int APS5_VABI sigtimedwait_nid_postfix(const GuestSignalSet* set, void* info, const KernelTimespec* timeout); @@ -770,6 +780,7 @@ unsigned int APS5_VABI sceKernelSleep(unsigned int seconds); [[noreturn]] void APS5_VABI _exit_nid_postfix(int status); [[gnu::returns_twice]] int APS5_VABI getcontext_nid_postfix(UserUcontext* context); +[[gnu::returns_twice]] int APS5_VABI swapcontext_nid_postfix(UserUcontext* oucp, const UserUcontext* ucp); [[gnu::returns_twice]] int APS5_VABI sigsetjmp_nid_postfix(std::uint64_t* buffer, int saveMask); [[noreturn]] void APS5_VABI siglongjmp_nid_postfix(std::uint64_t* buffer, int value); @@ -781,12 +792,15 @@ int APS5_VABI scePthreadCondattrGetpshared(const PthreadCondattr* attr, int* psh int APS5_VABI pthread_mutexattr_setkind_np_nid_postfix(PthreadMutexattr* attr, int kind); int APS5_VABI pthread_mutexattr_getkind_np_nid_postfix(PthreadMutexattr attr); int APS5_VABI scePthreadMutexattrGettype(const PthreadMutexattr* attr, int* type); +int APS5_VABI scePthreadMutexattrSetkind(PthreadMutexattr* attr, int kind); +int APS5_VABI scePthreadMutexattrGetkind(PthreadMutexattr attr); int APS5_VABI scePthreadMutexattrGetprotocol(const PthreadMutexattr* attr, int* protocol); int APS5_VABI scePthreadMutexattrSetprioceiling(PthreadMutexattr* attr, int prioceiling); int APS5_VABI scePthreadMutexattrGetprioceiling(const PthreadMutexattr* attr, int* prioceiling); int APS5_VABI scePthreadMutexattrSetpshared(PthreadMutexattr* attr, int pshared); int APS5_VABI scePthreadMutexattrGetpshared(const PthreadMutexattr* attr, int* pshared); int APS5_VABI pthread_rwlockattr_gettype_np_nid_postfix(const PthreadRwlockattr* attr, int* type); +int APS5_VABI pthread_rwlockattr_settype_np_nid_postfix(PthreadRwlockattr* attr, int type); int APS5_VABI scePthreadRwlockattrGettype(const PthreadRwlockattr* attr, int* type); int APS5_VABI scePthreadRwlockattrSetpshared(PthreadRwlockattr* attr, int pshared); int APS5_VABI scePthreadRwlockattrGetpshared(const PthreadRwlockattr* attr, int* pshared); diff --git a/core/libs/prx/libkernel_sys/CMakeLists.txt b/core/libs/prx/libkernel_sys/CMakeLists.txt index 8aead464a0..70631309ca 100644 --- a/core/libs/prx/libkernel_sys/CMakeLists.txt +++ b/core/libs/prx/libkernel_sys/CMakeLists.txt @@ -1,3 +1,3 @@ -add_library(${LIB_NAME} SHARED EXCLUDE_FROM_ALL Export.cpp Export.Discovery.cpp Aio.cpp) +add_library(${LIB_NAME} SHARED EXCLUDE_FROM_ALL Export.cpp Export.Discovery.cpp Aio.cpp ThreadQueries.cpp) target_include_directories(${LIB_NAME} PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(${LIB_NAME}) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 8fb821d7c1..2cb89a6cb3 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -1,3 +1,5 @@ +#include "prx/libkernel/KernelErrors.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" #include "prx/libkernel_sys/include/Api.hpp" #include "prx/libkernel/Pthread/include/Atfork.hpp" #include "prx/libkernel/include/Api.hpp" @@ -67,11 +69,6 @@ int APS5_VABI futimesat_nid_postfix(void) { return 0; } -int APS5_VABI get_authinfo_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI get_module_info_list_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -102,11 +99,6 @@ int APS5_VABI getfsstat_nid_postfix(void) { return 0; } -int APS5_VABI getlogin_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI ipmimgr_call_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -287,8 +279,10 @@ int APS5_VABI pthread_attr_setaffinity_np_nid_postfix(void) { return 0; } -int APS5_VABI pthread_attr_setcreatesuspend_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI pthread_attr_setcreatesuspend_np_nid_postfix(PthreadAttr* attr) { + constexpr int GuestEinval = 22; + if (!attr || !*attr) return GuestEinval; + (*attr)->_createSuspended = true; return 0; } @@ -302,11 +296,6 @@ int APS5_VABI pthread_barrier_setname_np_nid_postfix(void) { return 0; } -int APS5_VABI pthread_cond_setname_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI pthread_get_specificarray_np_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -322,13 +311,8 @@ int APS5_VABI pthread_getstack_np_nid_postfix(void) { return 0; } -int APS5_VABI pthread_main_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI pthread_multi_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); + pthread_resume_all_np_nid_postfix(); return 0; } @@ -362,14 +346,15 @@ int APS5_VABI pthread_mutexattr_setgen_np_nid_postfix(void) { return 0; } -int APS5_VABI pthread_resume_all_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI pthread_resume_all_np_nid_postfix(void) { + ResumeAllGuestThreads_nid_no_patch(); + scePthreadTestcancel(); } -int APS5_VABI pthread_resume_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI pthread_resume_np_nid_postfix(Pthread thread) { + constexpr int GuestEinval = 22; + if (!thread) return GuestEinval; + return ResumeGuestThread_nid_no_patch(thread); } int APS5_VABI pthread_resume_user_context_np_nid_postfix(void) { @@ -382,11 +367,6 @@ int APS5_VABI pthread_rwlock_setname_np_nid_postfix(void) { return 0; } -int APS5_VABI pthread_rwlockattr_settype_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI pthread_set_user_context_np_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -413,13 +393,11 @@ int APS5_VABI pthread_suspend_user_context_np_nid_postfix(void) { } int APS5_VABI pthread_switch_add_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 45; } int APS5_VABI pthread_switch_delete_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 45; } int APS5_VABI ptrace_nid_postfix(void) { @@ -493,7 +471,6 @@ int APS5_VABI sceKernelAddProcessToCanvasMap(void) { } int APS5_VABI sceKernelAddResource(void) { - NotImplemented_nid_no_patch(__func__); return 0; } @@ -573,13 +550,11 @@ int APS5_VABI sceKernelClearBootReqNotifyCount(void) { } int APS5_VABI sceKernelClearGameDirectMemory(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x8002004e); } int APS5_VABI sceKernelCloseEport(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80020001); } int APS5_VABI sceKernelCloseSema(void) { @@ -593,8 +568,7 @@ int APS5_VABI sceKernelCreateBudget(void) { } int APS5_VABI sceKernelCreateEport(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80020001); } int APS5_VABI sceKernelDebugAcquireAndUpdateDebugRegister(void) { @@ -673,8 +647,7 @@ int APS5_VABI sceKernelDeleteCpumodeEvent(void) { } int APS5_VABI sceKernelDeleteEport(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80020001); } int APS5_VABI sceKernelDeleteGpuExceptionEvent(void) { @@ -718,7 +691,6 @@ int APS5_VABI sceKernelEndAppMount2(void) { } int APS5_VABI sceKernelEventLogInit(void) { - NotImplemented_nid_no_patch(__func__); return 0; } @@ -992,11 +964,6 @@ int APS5_VABI sceKernelGetMemoryPstate(void) { return 0; } -int APS5_VABI sceKernelGetOpenPsIdForSystem(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelGetPagingStatsOfAllObjects(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1228,8 +1195,7 @@ int APS5_VABI sceKernelGiveDirectMemoryToMapper(void) { } int APS5_VABI sceKernelHasNeoMode(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 1; } int APS5_VABI sceKernelHwGetBdDriveInfo(void) { @@ -1268,8 +1234,7 @@ int APS5_VABI sceKernelIccGetBootTimestamp(void) { } int APS5_VABI sceKernelIccGetCPMode(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x8002002d); } int APS5_VABI sceKernelIccGetCountTime(void) { @@ -1363,8 +1328,7 @@ int APS5_VABI sceKernelIccSetBuzzer(void) { } int APS5_VABI sceKernelIccSetCPMode(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x8002002d); } int APS5_VABI sceKernelIccSetCpuInfoBit(void) { @@ -1418,13 +1382,7 @@ int APS5_VABI sceKernelIsAllowedToSelectDvdRegion(void) { } int APS5_VABI sceKernelIsAuthenticNeo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceKernelIsCEX(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 1; } int APS5_VABI sceKernelIsDebuggerAttached(void) { @@ -1432,11 +1390,6 @@ int APS5_VABI sceKernelIsDebuggerAttached(void) { return 0; } -int APS5_VABI sceKernelIsDevKit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelIsExperimentalBeta(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1453,13 +1406,11 @@ int APS5_VABI sceKernelIsGenuineDevKit(void) { } int APS5_VABI sceKernelIsGenuineKratosCex(void) { - NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceKernelIsGenuineN(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 1; } int APS5_VABI sceKernelIsGenuineTestKit(void) { @@ -1527,10 +1478,6 @@ int APS5_VABI sceKernelKernelHeapUsage(void) { return 0; } -int APS5_VABI sceKernelLoadStartModuleForSysmodule(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} int APS5_VABI sceKernelLoadStartModuleInternalForMono(void) { NotImplemented_nid_no_patch(__func__); @@ -1578,8 +1525,7 @@ int APS5_VABI sceKernelMapToolMemory(void) { } int APS5_VABI sceKernelMapTraceMemory(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x8002004e); } int APS5_VABI sceKernelMapperGetParam(void) { @@ -1627,8 +1573,7 @@ int APS5_VABI sceKernelNotifySystemSuspendStart(void) { } int APS5_VABI sceKernelOpenEport(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80020001); } int APS5_VABI sceKernelOpenInternal(void) { @@ -1672,8 +1617,7 @@ int APS5_VABI sceKernelQueryToolMemory(void) { } int APS5_VABI sceKernelQueryTraceMemory(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80020002); } int APS5_VABI sceKernelRandomizedPath(void) { @@ -1697,7 +1641,6 @@ int APS5_VABI sceKernelReleaseToolMemory(void) { } int APS5_VABI sceKernelReleaseTraceDirectMemory(void) { - NotImplemented_nid_no_patch(__func__); return 0; } @@ -2012,13 +1955,11 @@ int APS5_VABI sceKernelTitleWorkdaroundIsEnabled(void) { } int APS5_VABI sceKernelTraceMemoryTypeProtect(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x8002004e); } int APS5_VABI sceKernelTriggerEport(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80020001); } int APS5_VABI sceKernelVirtualQueryAll(void) { @@ -2149,9 +2090,9 @@ int APS5_VABI scePthreadAttrGetscope(const PthreadAttr* attr, int* scope) { return SceResult(pthread_attr_getscope_nid_postfix(attr, scope)); } -int APS5_VABI scePthreadAttrSetcreatesuspend(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadAttrSetcreatesuspend(PthreadAttr* attr) { + const int error = pthread_attr_setcreatesuspend_np_nid_postfix(attr); + return error == 0 ? 0 : SceKernelError(error); } int APS5_VABI scePthreadAttrSetscope(PthreadAttr* attr, int scope) { @@ -2196,14 +2137,10 @@ int APS5_VABI scePthreadGetcpuclockid(Pthread thread, int* clockId) { return SceResult(pthread_getcpuclockid_nid_postfix(thread, clockId)); } -int APS5_VABI scePthreadMain(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} int APS5_VABI scePthreadMulti(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + const int error = pthread_multi_np_nid_postfix(); + return error == 0 ? 0 : SceKernelError(error); } int APS5_VABI scePthreadMutexGetprioceiling(void) { @@ -2231,11 +2168,6 @@ int APS5_VABI scePthreadMutexattrGetgen(void) { return 0; } -int APS5_VABI scePthreadMutexattrGetkind(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI scePthreadMutexattrInitForInternalLibc(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -2246,19 +2178,13 @@ int APS5_VABI scePthreadMutexattrSetgen(void) { return 0; } -int APS5_VABI scePthreadMutexattrSetkind(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadResume(Pthread thread) { + const int error = pthread_resume_np_nid_postfix(thread); + return error == 0 ? 0 : SceKernelError(error); } -int APS5_VABI scePthreadResume(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI scePthreadResumeAll(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI scePthreadResumeAll(void) { + pthread_resume_all_np_nid_postfix(); } int APS5_VABI scePthreadSetBesteffort(void) { @@ -2320,16 +2246,6 @@ int APS5_VABI set_phys_fmem_limit_nid_postfix(void) { return 0; } -int APS5_VABI setlogin_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI settimeofday_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sigqueue_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -2345,11 +2261,6 @@ int APS5_VABI statfs_nid_postfix(void) { return 0; } -int APS5_VABI swapcontext_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI swapon_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 3c47113b4f..3157f7a862 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -62,6 +62,7 @@ static int GuestFstat(int fd, bool* withoutVnode = nullptr) { constexpr int GuestEnoent = 2; constexpr int GuestEperm = 1; +constexpr int GuestEsrch = 3; constexpr int GuestEio = 5; constexpr int GuestEbadf = 9; constexpr int GuestEfault = 14; @@ -82,14 +83,11 @@ constexpr std::size_t GuestPathMax = 1024; constexpr std::size_t GuestNameMax = 255; constexpr int PcLinkMax = 1; -constexpr int PcMaxCanon = 2; -constexpr int PcMaxInput = 3; constexpr int PcNameMax = 4; constexpr int PcPathMax = 5; constexpr int PcPipeBuf = 6; constexpr int PcChownRestricted = 7; constexpr int PcNoTrunc = 8; -constexpr int PcVdisable = 9; constexpr int PcAllocSizeMin = 10; constexpr int PcFileSizeBits = 12; constexpr int PcRecIncrXferSize = 14; @@ -131,7 +129,7 @@ std::int64_t Fail(int error) { return -1; } -std::int64_t PathConfigurationValue(int name, const char* function) { +std::int64_t PathConfigurationValue(int name) { switch (name) { case PcLinkMax: return 32767; case PcNameMax: return static_cast(GuestNameMax); @@ -144,24 +142,25 @@ std::int64_t PathConfigurationValue(int name, const char* function) { case PcSymlinkMax: return static_cast(GuestPathMax); case PcPrioIo: return 0; case PcSyncIo: return 0; - case PcMaxCanon: - case PcMaxInput: - case PcVdisable: - case PcAllocSizeMin: - case PcRecIncrXferSize: - case PcRecMinXferSize: - case PcRecXferAlign: - case PcMinHoleSize: - case PcAsyncIo: - case PcAclExtended: - case PcAclPathMax: - case PcMacPresent: - case PcAclNfs4: - throw std::runtime_error(std::string(function) + ": name " + std::to_string(name) + " depends on the console filesystem or kernel configuration"); + case PcAllocSizeMin: return 65536; + case PcRecIncrXferSize: return 65536; + case PcRecMinXferSize: return 65536; + case PcRecXferAlign: return 16384; + case PcMinHoleSize: return 65536; + case PcAclExtended: return 0; + case PcAclPathMax: return 3; + case PcMacPresent: return 0; + case PcAclNfs4: return 0; default: return Fail(GuestEinval); } } +int CopyPath(const char*& path, std::string& copy) { + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return error; + path = copy.c_str(); + return copy.empty() ? GuestEnoent : 0; +} + int GuestError(const std::error_code& error) { const auto condition = error.default_error_condition(); if (condition == std::errc::resource_unavailable_try_again) return GuestEagain; @@ -331,6 +330,37 @@ std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t mask) { return File::ExchangeCreationMask_nid_no_patch(mask); } +int APS5_VABI sceKernelGetOpenPsIdForSystem(void* id) { + if (id == nullptr) return GuestEinval; + return sceKernelGetOpenPsId(id); +} + +int APS5_VABI sceKernelIsCEX(void) { + return 1; +} + +int APS5_VABI sceKernelIsDevKit(void) { + return 0; +} + +int APS5_VABI get_authinfo_nid_postfix(int pid, KernelAuthInfo* info) { + if (info == nullptr) return static_cast(Fail(GuestEperm)); + if (pid != 0 && pid != getpid_nid_postfix()) return static_cast(Fail(GuestEsrch)); + *info = {}; + info->capabilities[0] = 0x2000000000000000; + return 0; +} + +int APS5_VABI setlogin_nid_postfix(const char* name) { + constexpr std::size_t MaxLoginName = 33; + std::string copy; + const int copied = CopyGuestString_nid_no_patch(name, MaxLoginName, copy); + if (copied == GuestEnametoolong) return static_cast(Fail(GuestEinval)); + if (copied != 0) return static_cast(Fail(copied)); + if (const int error = SetLogin_nid_no_patch(copy.c_str())) return static_cast(Fail(error)); + return 0; +} + int APS5_VABI fstatfs_nid_postfix(int d, KernelStatfs* buf) { return _fstatfs_nid_postfix(d, buf); } @@ -345,14 +375,18 @@ int APS5_VABI sceKernelGetModuleListInternal(KernelModule* handles, std::size_t return sceKernelGetModuleList(handles, count, actual); } +KernelModule APS5_VABI sceKernelLoadStartModuleForSysmodule(const char* moduleFileName, std::size_t args, const void* argp, std::uint32_t flags, const KernelLoadModuleOpt* opt, int* res) { + return sceKernelLoadStartModule(moduleFileName, args, argp, flags, opt, res); +} + int APS5_VABI fstat_nid_postfix(int d, FileStat* sb) { return static_cast(_fstat_nid_postfix(d, sb)); } std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std::size_t size) { if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); - if (path == nullptr) return Fail(GuestEfault); - if (*path == '\0') return Fail(GuestEnoent); + std::string copy; + if (const int error = CopyPath(path, copy)) return Fail(error); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return Fail(error); return ReadNativeLink(native, buffer, size); @@ -360,8 +394,8 @@ std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std: std::int64_t APS5_VABI readlinkat_nid_postfix(int fd, const char* path, char* buffer, std::size_t size) { if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); - if (path == nullptr) return Fail(GuestEfault); - if (*path == '\0') return Fail(GuestEnoent); + std::string copy; + if (const int error = CopyPath(path, copy)) return Fail(error); if (fd == GuestAtFdcwd || *path == '/') return readlink_nid_postfix(path, buffer, size); int error = 0; const char* base = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); @@ -371,8 +405,8 @@ std::int64_t APS5_VABI readlinkat_nid_postfix(int fd, const char* path, char* bu } std::int64_t APS5_VABI pathconf_nid_postfix(const char* path, int name) { - if (path == nullptr) return Fail(GuestEfault); - if (*path == '\0') return Fail(GuestEnoent); + std::string copy; + if (const int error = CopyPath(path, copy)) return Fail(error); if (std::strlen(path) >= GuestPathMax || HasLongComponent(path)) return Fail(GuestEnametoolong); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return Fail(error); @@ -380,7 +414,7 @@ std::int64_t APS5_VABI pathconf_nid_postfix(const char* path, int name) { const auto status = std::filesystem::status(native, error); if (error) return Fail(error == std::errc::no_such_file_or_directory && HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestError(error)); if (!std::filesystem::exists(status)) return Fail(HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestEnoent); - return PathConfigurationValue(name, "pathconf"); + return PathConfigurationValue(name); } std::int64_t APS5_VABI _fpathconf_nid_postfix(int fd, int name) { @@ -395,7 +429,7 @@ std::int64_t APS5_VABI _fpathconf_nid_postfix(int fd, int name) { #endif if (name == PcAsyncIo) return PosixAsynchronousIo; if (withoutVnode) return name == PcPipeBuf ? 512 : Fail(GuestEinval); - return PathConfigurationValue(name, "fpathconf"); + return PathConfigurationValue(name); } std::int64_t APS5_VABI fpathconf_nid_postfix(int fd, int name) { @@ -431,22 +465,21 @@ static int FollowLinkPath(const std::filesystem::path& source, std::filesystem:: } #endif -int APS5_VABI link_nid_postfix(const char* existing, const char* created) { - if (existing == nullptr || created == nullptr) return static_cast(Fail(GuestEfault)); - if (*existing == '\0' || *created == '\0') return static_cast(Fail(GuestEnoent)); - std::filesystem::path source, destination; - if (const int error = ResolvePathError_nid_no_patch(existing, source)) return static_cast(Fail(error)); - if (const int error = ResolvePathError_nid_no_patch(created, destination)) return static_cast(Fail(error)); +static int LinkSourceError(const std::filesystem::path& source) { std::error_code error; const auto status = std::filesystem::status(source, error); if (!std::filesystem::exists(status)) { #ifdef _WIN32 std::filesystem::path target; - if (const int followError = FollowLinkPath(source, target)) return static_cast(Fail(followError)); + if (const int followError = FollowLinkPath(source, target)) return followError; #endif - return static_cast(Fail(HasNonDirectoryPrefix(source) ? GuestEnotdir : GuestEnoent)); + return HasNonDirectoryPrefix(source) ? GuestEnotdir : GuestEnoent; } - if (std::filesystem::is_directory(status)) return static_cast(Fail(GuestEperm)); + return std::filesystem::is_directory(status) ? GuestEperm : 0; +} + +static int LinkNative(const std::filesystem::path& source, const std::filesystem::path& destination) { + std::error_code error; if (std::filesystem::exists(std::filesystem::symlink_status(destination, error))) return static_cast(Fail(GuestEexist)); if (!std::filesystem::is_directory(destination.parent_path(), error)) return static_cast(Fail(HasNonDirectoryPrefix(destination) ? GuestEnotdir : GuestEnoent)); #ifdef _WIN32 @@ -463,15 +496,38 @@ int APS5_VABI link_nid_postfix(const char* existing, const char* created) { return 0; } +int APS5_VABI link_nid_postfix(const char* existing, const char* created) { + std::string existingCopy, createdCopy; + 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 = 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)); + return LinkNative(source, destination); +} + +static int ResolveAtPath(int fd, const char* path, std::filesystem::path& native) { + if (fd == GuestAtFdcwd || *path == '/') return ResolvePathError_nid_no_patch(path, native); + if (std::strlen(path) >= GuestPathMax || HasLongComponent(path)) return GuestEnametoolong; + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); + if (base == nullptr) return error; + native = std::filesystem::path(reinterpret_cast(base)) / std::filesystem::path(reinterpret_cast(path)); + return 0; +} + int APS5_VABI linkat_nid_postfix(int fd1, const char* path1, int fd2, const char* path2, int flag) { if (flag & ~GuestAtSymlinkFollow) return static_cast(Fail(GuestEinval)); - if (path1 == nullptr || path2 == nullptr) return static_cast(Fail(GuestEfault)); - if ((fd1 != GuestAtFdcwd && *path1 != '/') || (fd2 != GuestAtFdcwd && *path2 != '/')) - NotImplemented_nid_no_patch("linkat: a relative path with a directory descriptor"); - std::filesystem::path source; - if (flag == 0 && *path1 != '\0' && ResolvePathError_nid_no_patch(path1, source) == 0 && IsSymbolicLink(source)) - NotImplemented_nid_no_patch("linkat: a hard link to a symbolic link itself"); - return link_nid_postfix(path1, path2); + std::string copy1, copy2; + 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 (flag == 0 && IsSymbolicLink(source)) NotImplemented_nid_no_patch("linkat: a hard link to a symbolic link itself"); + 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)); + return LinkNative(source, destination); } int APS5_VABI fchdir_nid_postfix(int fd) { @@ -487,9 +543,15 @@ int APS5_VABI fchdir_nid_postfix(int fd) { return 0; } +int SetTimeOfDay_nid_no_patch(const KernelTimeval* tv, const KernelTimezone* tz); + +int APS5_VABI settimeofday_nid_postfix(const KernelTimeval* tv, const KernelTimezone* tz) { + return SetTimeOfDay_nid_no_patch(tv, tz); +} + int APS5_VABI chroot_nid_postfix(const char* path) { - if (path == nullptr) return static_cast(Fail(GuestEfault)); - if (*path == '\0') return static_cast(Fail(GuestEnoent)); + std::string copy; + if (const int error = CopyPath(path, copy)) return static_cast(Fail(error)); if (std::strlen(path) >= GuestPathMax || HasLongComponent(path)) return static_cast(Fail(GuestEnametoolong)); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return static_cast(Fail(error)); @@ -505,9 +567,9 @@ int APS5_VABI chroot_nid_postfix(const char* path) { } int APS5_VABI symlink_nid_postfix(const char* target, const char* created) { - if (target == nullptr || created == nullptr) return static_cast(Fail(GuestEfault)); - if (std::strlen(target) >= GuestPathMax) return static_cast(Fail(GuestEnametoolong)); - if (*target == '\0' || *created == '\0') return static_cast(Fail(GuestEnoent)); + std::string targetCopy, createdCopy; + if (const int error = CopyPath(target, targetCopy)) return static_cast(Fail(error)); + if (const int error = CopyPath(created, createdCopy)) return static_cast(Fail(error)); const std::filesystem::path contents(target); if (contents.has_root_name() || contents.has_root_directory()) { NotImplemented_nid_no_patch("symlink: absolute targets"); @@ -566,8 +628,8 @@ int APS5_VABI openat_nid_postfix(int fd, const char* path, int flags, ...) { #endif } if (File::InvalidOpenFlags(flags)) return static_cast(Fail(GuestEinval)); - if (path == nullptr) return static_cast(Fail(GuestEfault)); - if (*path == '\0') return static_cast(Fail(GuestEnoent)); + std::string copy; + if (const int error = CopyPath(path, copy)) return static_cast(Fail(error)); if (fd == GuestAtFdcwd || *path == '/') return open_nid_postfix(path, flags, mode); const int result = OpenInDirectory_nid_no_patch(fd, path, flags, static_cast(mode)); if (result < 0) return static_cast(Fail(result & 0xffff)); @@ -594,8 +656,8 @@ int APS5_VABI _openat_nid_postfix(int fd, const char* path, int flags, ...) { int APS5_VABI mkdirat_nid_postfix(int fd, const char* path, std::uint16_t mode) { - if (path == nullptr) return static_cast(Fail(GuestEfault)); - if (*path == '\0') return static_cast(Fail(GuestEnoent)); + std::string copy; + if (const int error = CopyPath(path, copy)) return static_cast(Fail(error)); if (fd == GuestAtFdcwd || *path == '/') return mkdir_nid_postfix(path, mode); if (std::strlen(path) >= GuestPathMax || HasLongComponent(path)) return static_cast(Fail(GuestEnametoolong)); int error = 0; @@ -631,8 +693,8 @@ int APS5_VABI mkdirat_nid_postfix(int fd, const char* path, std::uint16_t mode) int APS5_VABI fchmodat_nid_postfix(int fd, const char* path, int mode, int flag) { if (flag != 0 && flag != GuestAtSymlinkNofollow) return static_cast(Fail(GuestEinval)); - if (path == nullptr) return static_cast(Fail(GuestEfault)); - if (*path == '\0') return static_cast(Fail(GuestEnoent)); + std::string copy; + if (const int error = CopyPath(path, copy)) return static_cast(Fail(error)); std::filesystem::path native; if (fd == GuestAtFdcwd || *path == '/') { if (const int error = ResolvePathError_nid_no_patch(path, native)) return static_cast(Fail(error)); @@ -670,9 +732,13 @@ int APS5_VABI fchmodat_nid_postfix(int fd, const char* path, int mode, int flag) } int APS5_VABI lchmod_nid_postfix(const char* path, std::uint16_t mode) { + std::string copy; + if (path != nullptr) { + if (const int error = CopyPath(path, copy)) return static_cast(Fail(error)); + } #ifndef __APPLE__ std::filesystem::path native; - if (path != nullptr && *path != '\0' && ResolvePathError_nid_no_patch(path, native) == 0) { + if (path != nullptr && ResolvePathError_nid_no_patch(path, native) == 0) { #ifdef _WIN32 const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); const bool symbolic = attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; @@ -687,15 +753,13 @@ int APS5_VABI lchmod_nid_postfix(const char* path, std::uint16_t mode) { } int APS5_VABI fchown_nid_postfix(int fd, std::uint32_t uid, std::uint32_t gid) { - constexpr std::uint32_t Unchanged = 0xFFFFFFFFu; - if (uid != 0 && uid != Unchanged) NotImplemented_nid_no_patch("fchown: an owner other than the guest user"); - if (gid != 0 && gid != Unchanged) NotImplemented_nid_no_patch("fchown: a group other than the guest group"); #ifdef _WIN32 if (GuestFstat(fd) != 0) return static_cast(Fail(GuestEbadf)); #else struct stat status{}; if (::fstat(fd, &status) != 0) return static_cast(Fail(GuestEbadf)); #endif + RequireGuestOwner("fchown", uid, gid); return 0; } @@ -733,8 +797,8 @@ int APS5_VABI fchownat_nid_postfix(int fd, const char* path, std::uint32_t uid, int APS5_VABI fstatat_nid_postfix(int fd, const char* path, FileStat* sb, int flag) { if (flag & ~GuestAtSymlinkNofollow) return static_cast(Fail(GuestEinval)); - if (path == nullptr) return static_cast(Fail(GuestEfault)); - if (*path == '\0') return static_cast(Fail(GuestEnoent)); + std::string copy; + if (const int error = CopyPath(path, copy)) return static_cast(Fail(error)); if (fd == GuestAtFdcwd || *path == '/') return flag ? lstat_nid_postfix(path, sb) : stat_nid_postfix(path, sb); const int result = StatInDirectory_nid_no_patch(fd, path, sb, flag == 0); if (result < 0) return static_cast(Fail(result & 0xffff)); @@ -743,8 +807,8 @@ int APS5_VABI fstatat_nid_postfix(int fd, const char* path, FileStat* sb, int fl int APS5_VABI unlinkat_nid_postfix(int fd, const char* path, int flag) { if (flag & ~GuestAtRemovedir) return static_cast(Fail(GuestEinval)); - if (path == nullptr) return static_cast(Fail(GuestEfault)); - if (*path == '\0') return static_cast(Fail(GuestEnoent)); + std::string copy; + if (const int error = CopyPath(path, copy)) return static_cast(Fail(error)); if (fd == GuestAtFdcwd || *path == '/') return (flag & GuestAtRemovedir) ? rmdir_nid_postfix(path) : unlink_nid_postfix(path); if (std::strlen(path) >= GuestPathMax || HasLongComponent(path)) return static_cast(Fail(GuestEnametoolong)); int error = 0; @@ -820,14 +884,13 @@ int APS5_VABI scePthreadCondattrGetclock(const PthreadCondattr* attr, KernelCloc } int APS5_VABI renameat_nid_postfix(int oldfd, const char* from, int newfd, const char* to) { - if (from == nullptr || to == nullptr) return static_cast(Fail(GuestEfault)); - if (*from == '\0' || *to == '\0') return static_cast(Fail(GuestEnoent)); - if ((oldfd == GuestAtFdcwd || *from == '/') && (newfd == GuestAtFdcwd || *to == '/')) + std::string fromCopy, toCopy; + if (const int error = CopyPath(from, fromCopy)) return static_cast(Fail(error)); + if ((oldfd == GuestAtFdcwd || *from == '/') && newfd == GuestAtFdcwd) return RenameGuestPath_nid_no_patch(from, to); - if (std::strlen(from) >= GuestPathMax || HasLongComponent(from) || - std::strlen(to) >= GuestPathMax || HasLongComponent(to)) + if (std::strlen(from) >= GuestPathMax || HasLongComponent(from)) return static_cast(Fail(GuestEnametoolong)); - if (IsDotOrDotDot(from) || IsDotOrDotDot(to)) return static_cast(Fail(GuestEinval)); + if (IsDotOrDotDot(from)) return static_cast(Fail(GuestEinval)); std::filesystem::path source; if (oldfd == GuestAtFdcwd || *from == '/') { @@ -839,6 +902,20 @@ int APS5_VABI renameat_nid_postfix(int oldfd, const char* from, int newfd, const source = std::filesystem::path(reinterpret_cast(base)) / std::filesystem::path(reinterpret_cast(from)); } + const std::string_view fromView(from); + const bool oldTrailingSlash = !fromView.empty() && fromView.back() == '/'; + while (!source.has_filename() && source.has_relative_path()) source = source.parent_path(); + std::error_code statusError; + const auto sourceStatus = std::filesystem::symlink_status(source, statusError); + if (statusError && statusError != std::errc::no_such_file_or_directory) + return static_cast(Fail(FilesystemError(statusError))); + if (sourceStatus.type() == std::filesystem::file_type::not_found) + return static_cast(Fail(HasNonDirectoryPrefix(source) ? GuestEnotdir : GuestEnoent)); + if (oldTrailingSlash && !std::filesystem::is_directory(sourceStatus)) return static_cast(Fail(GuestEnotdir)); + + if (const int error = CopyPath(to, toCopy)) return static_cast(Fail(error)); + if (std::strlen(to) >= GuestPathMax || HasLongComponent(to)) return static_cast(Fail(GuestEnametoolong)); + if (IsDotOrDotDot(to)) return static_cast(Fail(GuestEinval)); std::filesystem::path destination; if (newfd == GuestAtFdcwd || *to == '/') { @@ -851,21 +928,11 @@ int APS5_VABI renameat_nid_postfix(int oldfd, const char* from, int newfd, const std::filesystem::path(reinterpret_cast(to)); } - const std::string_view fromView(from); const std::string_view toView(to); - const bool oldTrailingSlash = !fromView.empty() && fromView.back() == '/'; const bool newTrailingSlash = !toView.empty() && toView.back() == '/'; - while (!source.has_filename() && source.has_relative_path()) source = source.parent_path(); while (!destination.has_filename() && destination.has_relative_path()) destination = destination.parent_path(); - std::error_code statusError; - const auto sourceStatus = std::filesystem::symlink_status(source, statusError); - if (oldTrailingSlash) { - if (sourceStatus.type() == std::filesystem::file_type::not_found) - return static_cast(Fail(HasNonDirectoryPrefix(source) ? GuestEnotdir : GuestEnoent)); - if (!std::filesystem::is_directory(sourceStatus)) return static_cast(Fail(GuestEnotdir)); - } if (newTrailingSlash && sourceStatus.type() != std::filesystem::file_type::not_found && !std::filesystem::is_directory(sourceStatus)) return static_cast(Fail(GuestEnotdir)); diff --git a/core/libs/prx/libkernel_sys/ThreadQueries.cpp b/core/libs/prx/libkernel_sys/ThreadQueries.cpp new file mode 100644 index 0000000000..397716777c --- /dev/null +++ b/core/libs/prx/libkernel_sys/ThreadQueries.cpp @@ -0,0 +1,14 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +extern "C" { +bool IsInitialPthread_nid_no_patch(); + +int APS5_VABI pthread_main_np_nid_postfix() { + return IsInitialPthread_nid_no_patch() ? 1 : 0; +} + +int APS5_VABI scePthreadMain(void) { + return pthread_main_np_nid_postfix(); +} + +} diff --git a/core/libs/prx/libkernel_sys/include/Api.hpp b/core/libs/prx/libkernel_sys/include/Api.hpp index 938bc8178b..ae38852b68 100644 --- a/core/libs/prx/libkernel_sys/include/Api.hpp +++ b/core/libs/prx/libkernel_sys/include/Api.hpp @@ -24,6 +24,14 @@ struct KernelTitleWorkaround { std::uint64_t ids[2]; }; +struct KernelAuthInfo { + std::uint64_t programAuthorityId; + std::uint64_t capabilities[4]; + std::uint64_t attributes[4]; + std::uint64_t credentials[8]; +}; +static_assert(sizeof(KernelAuthInfo) == 0x88); + extern "C" { int APS5_VABI aio_read_nid_postfix(GuestAiocb* aiocb); @@ -41,6 +49,7 @@ std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t mask); int APS5_VABI fstatfs_nid_postfix(int d, KernelStatfs* buf); int APS5_VABI sceKernelGetModuleInfoInternal(KernelModule handle, ModuleInfoEx* info); int APS5_VABI sceKernelGetModuleListInternal(KernelModule* handles, std::size_t count, std::size_t* actual); +KernelModule APS5_VABI sceKernelLoadStartModuleForSysmodule(const char* moduleFileName, std::size_t args, const void* argp, std::uint32_t flags, const KernelLoadModuleOpt* opt, int* res); int APS5_VABI fstat_nid_postfix(int d, FileStat* sb); std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std::size_t size); std::int64_t APS5_VABI readlinkat_nid_postfix(int fd, const char* path, char* buffer, std::size_t size); @@ -62,6 +71,7 @@ int APS5_VABI chown_nid_postfix(const char* path, std::uint32_t uid, std::uint32 int APS5_VABI lchown_nid_postfix(const char* path, std::uint32_t uid, std::uint32_t gid); int APS5_VABI fchownat_nid_postfix(int fd, const char* path, std::uint32_t uid, std::uint32_t gid, int flag); int APS5_VABI fstatat_nid_postfix(int fd, const char* path, FileStat* sb, int flag); +int APS5_VABI settimeofday_nid_postfix(const KernelTimeval* tv, const KernelTimezone* tz); int APS5_VABI unlinkat_nid_postfix(int fd, const char* path, int flag); int APS5_VABI scePthreadCondattrGetclock(const PthreadCondattr* attr, KernelClockid* clockId); int APS5_VABI renameat_nid_postfix(int oldfd, const char* from, int newfd, const char* to); @@ -80,14 +90,13 @@ int APS5_VABI cpuset_getid_nid_postfix(void); int APS5_VABI cpuset_setid_nid_postfix(void); int APS5_VABI dynlib_get_obj_member_nid_postfix(void); int APS5_VABI futimesat_nid_postfix(void); -int APS5_VABI get_authinfo_nid_postfix(void); +int APS5_VABI get_authinfo_nid_postfix(int pid, KernelAuthInfo* info); int APS5_VABI get_module_info_list_nid_postfix(void); int APS5_VABI get_page_table_stats_nid_postfix(void); int APS5_VABI get_sdk_compiled_version_nid_postfix(void); int APS5_VABI get_self_auth_info_nid_postfix(void); int APS5_VABI get_vm_map_timestamp_nid_postfix(void); int APS5_VABI getfsstat_nid_postfix(void); -int APS5_VABI getlogin_nid_postfix(void); int APS5_VABI ipmimgr_call_nid_postfix(void); int APS5_VABI is_in_sandbox_nid_postfix(void); int APS5_VABI kenv_nid_postfix(void); @@ -124,10 +133,9 @@ int APS5_VABI profil_nid_postfix(void); int APS5_VABI pthread_attr_getaffinity_np_nid_postfix(void); int APS5_VABI pthread_attr_getsolosched_np_nid_postfix(void); int APS5_VABI pthread_attr_setaffinity_np_nid_postfix(void); -int APS5_VABI pthread_attr_setcreatesuspend_np_nid_postfix(void); +int APS5_VABI pthread_attr_setcreatesuspend_np_nid_postfix(PthreadAttr* attr); int APS5_VABI pthread_attr_setstackaddr_nid_postfix(void); int APS5_VABI pthread_barrier_setname_np_nid_postfix(void); -int APS5_VABI pthread_cond_setname_np_nid_postfix(void); int APS5_VABI pthread_get_specificarray_np_nid_postfix(void); int APS5_VABI pthread_get_user_context_np_nid_postfix(void); int APS5_VABI pthread_getstack_np_nid_postfix(void); @@ -139,11 +147,10 @@ int APS5_VABI pthread_mutex_setname_np_nid_postfix(void); int APS5_VABI pthread_mutex_setprioceiling_nid_postfix(void); int APS5_VABI pthread_mutexattr_getgen_np_nid_postfix(void); int APS5_VABI pthread_mutexattr_setgen_np_nid_postfix(void); -int APS5_VABI pthread_resume_all_np_nid_postfix(void); -int APS5_VABI pthread_resume_np_nid_postfix(void); +void APS5_VABI pthread_resume_all_np_nid_postfix(void); +int APS5_VABI pthread_resume_np_nid_postfix(Pthread thread); int APS5_VABI pthread_resume_user_context_np_nid_postfix(void); int APS5_VABI pthread_rwlock_setname_np_nid_postfix(void); -int APS5_VABI pthread_rwlockattr_settype_np_nid_postfix(void); int APS5_VABI pthread_set_user_context_np_nid_postfix(void); int APS5_VABI pthread_single_np_nid_postfix(void); int APS5_VABI pthread_suspend_all_np_nid_postfix(void); @@ -265,7 +272,7 @@ int APS5_VABI sceKernelGetIpcPath(void); int APS5_VABI sceKernelGetKnobs(void); int APS5_VABI sceKernelGetKqueueFromEqueue(void); int APS5_VABI sceKernelGetMemoryPstate(void); -int APS5_VABI sceKernelGetOpenPsIdForSystem(void); +int APS5_VABI sceKernelGetOpenPsIdForSystem(void* id); int APS5_VABI sceKernelGetPagingStatsOfAllObjects(void); int APS5_VABI sceKernelGetPagingStatsOfAllThreads(void); int APS5_VABI sceKernelGetParentApp(void); @@ -372,7 +379,6 @@ int APS5_VABI sceKernelIsPs4Process(void); int APS5_VABI sceKernelIsTestKit(void); int APS5_VABI sceKernelJitGetSharedMemoryInfo(void); int APS5_VABI sceKernelKernelHeapUsage(void); -int APS5_VABI sceKernelLoadStartModuleForSysmodule(void); int APS5_VABI sceKernelLoadStartModuleInternalForMono(void); int APS5_VABI sceKernelLwfsAllocateBlock(void); int APS5_VABI sceKernelLwfsLseek(void); @@ -497,7 +503,7 @@ int APS5_VABI sceKernelYieldCpumode(void); int APS5_VABI sceLibcMspaceCreateForMonoMutex(void); int APS5_VABI scePthreadAtfork(void (APS5_VABI* prepare)(), void (APS5_VABI* parent)(), void (APS5_VABI* child)()); int APS5_VABI scePthreadAttrGetscope(const PthreadAttr* attr, int* scope); -int APS5_VABI scePthreadAttrSetcreatesuspend(void); +int APS5_VABI scePthreadAttrSetcreatesuspend(PthreadAttr* attr); int APS5_VABI scePthreadAttrSetscope(PthreadAttr* attr, int scope); int APS5_VABI scePthreadBarrierDestroy(PthreadBarrierPrivate** barrier); int APS5_VABI scePthreadBarrierInit(PthreadBarrierPrivate** barrier, PthreadBarrierattrPrivate* const* attr, unsigned count, const char* name); @@ -515,12 +521,10 @@ int APS5_VABI scePthreadMutexInitForInternalLibc(void); int APS5_VABI scePthreadMutexIsowned(void); int APS5_VABI scePthreadMutexSetprioceiling(void); int APS5_VABI scePthreadMutexattrGetgen(void); -int APS5_VABI scePthreadMutexattrGetkind(void); int APS5_VABI scePthreadMutexattrInitForInternalLibc(void); int APS5_VABI scePthreadMutexattrSetgen(void); -int APS5_VABI scePthreadMutexattrSetkind(void); -int APS5_VABI scePthreadResume(void); -int APS5_VABI scePthreadResumeAll(void); +int APS5_VABI scePthreadResume(Pthread thread); +void APS5_VABI scePthreadResumeAll(void); int APS5_VABI scePthreadSetBesteffort(void); int APS5_VABI scePthreadSetconcurrency(int level); int APS5_VABI scePthreadSingle(void); @@ -533,12 +537,10 @@ int APS5_VABI sched_setparam_nid_postfix(void); int APS5_VABI sched_setscheduler_nid_postfix(void); int APS5_VABI sem_setname_nid_postfix(void); int APS5_VABI set_phys_fmem_limit_nid_postfix(void); -int APS5_VABI setlogin_nid_postfix(void); -int APS5_VABI settimeofday_nid_postfix(void); +int APS5_VABI setlogin_nid_postfix(const char* name); int APS5_VABI sigqueue_nid_postfix(void); int APS5_VABI sigreturn_nid_postfix(void); int APS5_VABI statfs_nid_postfix(void); -int APS5_VABI swapcontext_nid_postfix(void); int APS5_VABI swapon_nid_postfix(void); int APS5_VABI symlinkat_nid_postfix(void); int APS5_VABI sysKernelGetIntdevModeForInternalLibc_nid_postfix(void); diff --git a/core/libs/prx/libmdbg_syscore/Export.cpp b/core/libs/prx/libmdbg_syscore/Export.cpp index 77409149dd..98fb4bdf87 100644 --- a/core/libs/prx/libmdbg_syscore/Export.cpp +++ b/core/libs/prx/libmdbg_syscore/Export.cpp @@ -227,8 +227,7 @@ int APS5_VABI sceDebugGetProcessCoredumpHandlerResult() { } int APS5_VABI sceDebugGetProcessEventCntlFlag() { - NotImplemented_nid_no_patch(__func__); - return 0; + return 1; } int APS5_VABI sceDebugGetProcessInfo() { @@ -652,8 +651,7 @@ int APS5_VABI sceDebugResumeThread() { } int APS5_VABI sceDebugSetProcessEventCntlFlag() { - NotImplemented_nid_no_patch(__func__); - return 0; + return 1; } int APS5_VABI sceDebugSetStepThread() { diff --git a/core/libs/prx/ulobjmgr/Export.cpp b/core/libs/prx/ulobjmgr/Export.cpp index 93b5fa3ddb..31328f15db 100644 --- a/core/libs/prx/ulobjmgr/Export.cpp +++ b/core/libs/prx/ulobjmgr/Export.cpp @@ -24,4 +24,12 @@ int APS5_VABI _sceUlobjmgrUnregisterObject(std::uint32_t id) { return 0; } +int APS5_VABI _sceUlobjmgrRegisterObject_nid_postfix(std::uint64_t object, std::int32_t kind, std::uint32_t* id) { + return _sceUlobjmgrRegisterObject(object, kind, id); +} + +int APS5_VABI _sceUlobjmgrUnregisterObject_nid_postfix(std::uint32_t id) { + return _sceUlobjmgrUnregisterObject(id); +} + } diff --git a/core/libs/prx/ulobjmgr/include/Api.hpp b/core/libs/prx/ulobjmgr/include/Api.hpp index 2f2e574e66..f551d92f17 100644 --- a/core/libs/prx/ulobjmgr/include/Api.hpp +++ b/core/libs/prx/ulobjmgr/include/Api.hpp @@ -7,4 +7,7 @@ extern "C" { int APS5_VABI _sceUlobjmgrRegisterObject(std::uint64_t object, std::int32_t kind, std::uint32_t* id); int APS5_VABI _sceUlobjmgrUnregisterObject(std::uint32_t id); +int APS5_VABI _sceUlobjmgrRegisterObject_nid_postfix(std::uint64_t object, std::int32_t kind, std::uint32_t* id); +int APS5_VABI _sceUlobjmgrUnregisterObject_nid_postfix(std::uint32_t id); + } diff --git a/core/libs/tests/ExceptionRuntime.cpp b/core/libs/tests/ExceptionRuntime.cpp index 8eb7fde24e..633a2d2005 100644 --- a/core/libs/tests/ExceptionRuntime.cpp +++ b/core/libs/tests/ExceptionRuntime.cpp @@ -40,6 +40,7 @@ struct Virtual : virtual Base {}; struct Left : Base {}; struct Right : Base {}; struct Repeated : Left, Right {}; +struct MixedRepeated : Left, private Right {}; struct LeftVirtual : virtual Base {}; struct RightVirtual : virtual Base {}; struct Diamond : LeftVirtual, RightVirtual {}; @@ -59,6 +60,9 @@ void CheckRtti() { try { throw static_cast(nullptr); } catch (Base*) { assert(false); } catch (Repeated* p) { assert(p == nullptr); } + try { throw MixedRepeated(); } + catch (Base&) { assert(false); } + catch (MixedRepeated&) {} try { throw static_cast(nullptr); } catch (Base* p) { assert(p == nullptr); } try { throw nullptr; } diff --git a/core/libs/tests/Fiber.cpp b/core/libs/tests/Fiber.cpp index 9f77bd063e..b66f020ff5 100644 --- a/core/libs/tests/Fiber.cpp +++ b/core/libs/tests/Fiber.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #ifdef _WIN32 @@ -310,6 +311,195 @@ void RunInternalOptions() { Require(sceFiberFinalize(fiber) == 0, "finalize fpu defaults fiber"); } +std::array g_contextlessStorage; +std::array g_contextlessEntries{}; +alignas(16) std::array g_transitionContext; +FiberStorage g_transitionStorage; +auto* const g_transition = reinterpret_cast(&g_transitionStorage); +std::uintptr_t g_callerStack = 0; +std::uint64_t g_contextlessFramePointer = 0; +unsigned g_contextlessHops = 0; +#ifdef _WIN32 +void* g_callerBase = nullptr; +void* g_callerLimit = nullptr; +#endif + +FiberObject* Contextless(unsigned index) { + return reinterpret_cast(&g_contextlessStorage[index]); +} + +void APS5_VABI ContextlessEntry(std::uint64_t index, std::uint64_t run) { + ++g_contextlessEntries[index]; + auto* self = Contextless(index); + FiberObject* current = nullptr; + Require(sceFiberGetSelf(¤t) == 0 && current == self, "contextless entry self"); + FiberInfo info{}; + info.size = sizeof(info); + Require(sceFiberGetInfo(self, &info) == 0, "contextless entry info"); + if (info.addr_context) { + Require(run == 90, "attaching after contextless execution restarts entry"); + for (;;) sceFiberReturnToThread(run + 1, &run); + } + Require(info.size_context == 0 && info.size_context_margin == ~0ull, "contextless info has no stack or margin"); + std::uint64_t framePointer = 0; + Require(sceFiberGetThreadFramePointerAddress(&framePointer) == 0 && framePointer == g_contextlessFramePointer, "contextless entry preserves the caller frame pointer"); + Require(sceFiberRun_nid_postfix(self, 0, nullptr) == FiberErrorPermission, "contextless entry cannot run a fiber"); + Require(sceFiberSwitch(self, 0, nullptr) == FiberErrorState, "contextless entry cannot switch to itself"); + Require(sceFiberFinalize(self) == FiberErrorState, "contextless entry cannot finalize itself"); + volatile std::uint64_t marker = 0; + const auto address = reinterpret_cast(&marker); + Require(address < g_callerStack && g_callerStack - address < 32768, "contextless execution uses calling thread stack"); + _mm_setcsr(_mm_getcsr() ^ 0x8000u); +#ifdef _WIN32 + const auto* tib = reinterpret_cast(NtCurrentTeb()); + Require(tib->StackBase == g_callerBase && tib->StackLimit == g_callerLimit, "contextless execution restores thread stack bounds"); +#endif + if (run == 2) { + sceFiberSwitch(Contextless(1), 1, nullptr); + } else if (run == 3) { + _sceFiberAttachContextAndSwitch(g_transition, g_transitionContext.data(), g_transitionContext.size(), 50, nullptr); + } else if (run == 4) { + sceFiberSwitch(g_transition, 70, nullptr); + } else if (run == 5) { + if (++g_contextlessHops < 1024) sceFiberSwitch(Contextless(1 - index), 5, nullptr); + sceFiberReturnToThread(999, nullptr); + } else { + Require(run == 1, "contextless run argument"); + sceFiberReturnToThread(200 + index, nullptr); + } + Require(false, "contextless fiber must restart rather than resume after yielding"); +} + +void APS5_VABI TransitionEntry(std::uint64_t, std::uint64_t run) { + Require(run == 50, "contextless source attaches target"); + Require(sceFiberReturnToThread(777, &run) == 0 && run == 55, "attached target resumes after contextless source"); + Require(_sceFiberAttachContextAndSwitch(Contextless(1), nullptr, 0, 1, &run) == 0 && run == 60, "attached source resumes after contextless target"); + Require(sceFiberSwitch(Contextless(0), 4, &run) == 0 && run == 70, "contextless target switches back to suspended attached source"); + for (;;) sceFiberReturnToThread(778, nullptr); +} + +[[gnu::noinline]] std::int32_t RunContextless(FiberObject* fiber, std::uint64_t run, std::uint64_t* returned, bool attach = false) { + register std::uint64_t savedRbx asm("rbx") = 0x123456789abcdef0ull; + register std::uint64_t savedR12 asm("r12") = 0x23456789abcdef01ull; + register std::uint64_t savedR13 asm("r13") = 0x3456789abcdef012ull; + register std::uint64_t savedR14 asm("r14") = 0x456789abcdef0123ull; + register std::uint64_t savedR15 asm("r15") = 0x56789abcdef01234ull; + asm volatile("" : "+r"(savedRbx), "+r"(savedR12), "+r"(savedR13), "+r"(savedR14), "+r"(savedR15)); + g_contextlessFramePointer = reinterpret_cast(__builtin_frame_address(0)); + const auto result = attach ? _sceFiberAttachContextAndRun(fiber, nullptr, 0, run, returned) : sceFiberRun_nid_postfix(fiber, run, returned); + asm volatile("" : "+r"(savedRbx), "+r"(savedR12), "+r"(savedR13), "+r"(savedR14), "+r"(savedR15) : : "memory"); + Require(savedRbx == 0x123456789abcdef0ull && savedR12 == 0x23456789abcdef01ull && savedR13 == 0x3456789abcdef012ull && savedR14 == 0x456789abcdef0123ull && savedR15 == 0x56789abcdef01234ull, "contextless return restores caller nonvolatile registers"); + return result; +} + +void RunContextlessTransitions() { + volatile std::uint64_t marker = 0; + g_callerStack = reinterpret_cast(&marker); +#ifdef _WIN32 + const auto* tib = reinterpret_cast(NtCurrentTeb()); + g_callerBase = tib->StackBase; + g_callerLimit = tib->StackLimit; +#endif + for (unsigned index = 0; index < 2; ++index) { + Require(_sceFiberInitializeWithInternalOptionImpl(Contextless(index), "contextless", ContextlessEntry, index, nullptr, 0, nullptr, 0x10, 0) == 0, "initialize contextless transition fiber with size checking"); + } + Require(_sceFiberInitializeImpl_nid_postfix(g_transition, "transition", TransitionEntry, 0, nullptr, 0, nullptr, 0) == 0, "initialize transition target"); + std::uint64_t returned = 0; + const auto csr = _mm_getcsr(); + Require(RunContextless(Contextless(0), 1, &returned) == 0 && returned == 200, "first contextless run"); + Require(_mm_getcsr() == csr, "contextless return restores thread MXCSR"); + Require(RunContextless(Contextless(0), 1, &returned) == 0 && returned == 200 && g_contextlessEntries[0] == 2, "contextless return discards continuation"); + Require(RunContextless(Contextless(0), 2, &returned, true) == 0 && returned == 201, "contextless to contextless switch"); + Require(RunContextless(Contextless(1), 1, &returned) == 0 && returned == 201 && g_contextlessEntries[1] == 2, "contextless switch target restarts"); + Require(RunContextless(Contextless(0), 3, &returned) == 0 && returned == 777, "contextless to attached switch"); + Require(RunContextless(g_transition, 55, &returned) == 0 && returned == 201, "attached to contextless switch"); + Require(RunContextless(g_transition, 60, &returned) == 0 && returned == 778, "switch through contextless entry back to attached fiber"); + Require(_sceFiberAttachContextAndRun(Contextless(0), g_firstContext.data(), g_firstContext.size(), 90, &returned) == 0 && returned == 91, "attach after abandoning contextless stack"); + Require(sceFiberRun_nid_postfix(Contextless(0), 100, &returned) == 0 && returned == 101 && g_contextlessEntries[0] == 6, "attached continuation persists after contextless runs"); + std::thread([&] { + volatile std::uint64_t workerMarker = 0; + g_callerStack = reinterpret_cast(&workerMarker); +#ifdef _WIN32 + const auto* workerTib = reinterpret_cast(NtCurrentTeb()); + g_callerBase = workerTib->StackBase; + g_callerLimit = workerTib->StackLimit; +#endif + Require(RunContextless(Contextless(1), 1, &returned) == 0 && returned == 201 && g_contextlessEntries[1] == 4, "contextless fiber restarts on another thread's stack"); + }).join(); + Require(sceFiberFinalize(Contextless(0)) == 0 && sceFiberFinalize(Contextless(1)) == 0 && sceFiberFinalize(g_transition) == 0, "finalize contextless transition fibers"); + g_callerStack = reinterpret_cast(&marker); +#ifdef _WIN32 + g_callerBase = tib->StackBase; + g_callerLimit = tib->StackLimit; +#endif + for (unsigned index = 0; index < 2; ++index) { + Require(_sceFiberInitializeImpl_nid_postfix(Contextless(index), "restart loop", ContextlessEntry, index, nullptr, 0, nullptr, 0) == 0, "initialize contextless restart loop"); + } + Require(RunContextless(Contextless(0), 5, &returned) == 0 && returned == 999 && g_contextlessHops == 1024, "contextless switch loop reuses the caller stack"); + Require(RunContextless(Contextless(1), 1, nullptr, true) == 0, "contextless run permits a null return output"); + Require(sceFiberFinalize(Contextless(0)) == 0 && sceFiberFinalize(Contextless(1)) == 0, "finalize contextless restart loop"); +} + +std::uint16_t ReadFpuControl() { + std::uint16_t control = 0; + asm volatile("fnstcw %0" : "=m"(control)); + return control; +} + +void WriteFpuControl(std::uint16_t control) { + asm volatile("fldcw %0" : : "m"(control)); +} + +constexpr std::uint64_t ContextlessInitializeArgument = 0xfedcba9876543210ull; +constexpr std::uint64_t ContextlessRunArgument = 0x8123456789abcdefull; +constexpr std::uint32_t ContextlessCsr = 0x7f80; +constexpr std::uint16_t ContextlessControl = 0x0b7f; +unsigned g_contextlessOptionEntries = 0; + +void APS5_VABI ContextlessOptionsEntry(std::uint64_t initialized, std::uint64_t run) { + Require(initialized == ContextlessInitializeArgument && run == ContextlessRunArgument, "contextless entry keeps 64-bit arguments"); + ++g_contextlessOptionEntries; + const auto result = (static_cast(ReadFpuControl()) << 32) | _mm_getcsr(); + _mm_setcsr(0x1f80); + WriteFpuControl(0x037f); + sceFiberReturnToThread(result, nullptr); + Require(false, "contextless options entry cannot resume after returning"); +} + +void APS5_VABI ContextlessFpuSource(std::uint64_t, std::uint64_t target) { + _mm_setcsr(ContextlessCsr); + WriteFpuControl(ContextlessControl); + sceFiberSwitch(reinterpret_cast(target), ContextlessRunArgument, nullptr); + Require(false, "contextless FPU source is not resumed"); +} + +void RunContextlessOptions() { + FiberStorage storage{}; + FiberStorage sourceStorage{}; + auto* fiber = reinterpret_cast(&storage); + auto* source = reinterpret_cast(&sourceStorage); + const auto callerCsr = _mm_getcsr(); + const auto callerControl = ReadFpuControl(); + for (const auto flags : {0u, 0x100u}) { + Require(_sceFiberInitializeWithInternalOptionImpl(fiber, "contextless options", ContextlessOptionsEntry, ContextlessInitializeArgument, nullptr, 0, nullptr, flags, 0) == 0, "initialize contextless FPU options"); + _mm_setcsr(ContextlessCsr); + WriteFpuControl(ContextlessControl); + const auto expected = flags ? (0x037full << 32) | 0x9fc0u : (static_cast(ContextlessControl) << 32) | ContextlessCsr; + std::uint64_t returned = 0; + for (unsigned restart = 0; restart < 2; ++restart) { + Require(sceFiberRun_nid_postfix(fiber, ContextlessRunArgument, &returned) == 0 && returned == expected, "contextless FPU policy applies on each run"); + Require(_mm_getcsr() == ContextlessCsr && ReadFpuControl() == ContextlessControl, "contextless return restores caller FPU registers"); + } + _mm_setcsr(callerCsr); + WriteFpuControl(callerControl); + Require(_sceFiberInitializeImpl_nid_postfix(source, "FPU source", ContextlessFpuSource, 0, g_transitionContext.data(), g_transitionContext.size(), nullptr, 0) == 0, "initialize attached FPU source"); + Require(sceFiberRun_nid_postfix(source, reinterpret_cast(fiber), &returned) == 0 && returned == expected, "contextless switch inherits or resets the source FPU registers"); + Require(_mm_getcsr() == callerCsr && ReadFpuControl() == callerControl, "contextless switch restores the original caller FPU registers"); + Require(sceFiberFinalize(fiber) == 0 && sceFiberFinalize(source) == 0, "finalize contextless FPU options"); + } + Require(g_contextlessOptionEntries == 6, "contextless FPU entry restarts on runs and switches"); +} + [[gnu::noinline]] std::int32_t RunFirst(std::uint64_t* returned) { g_threadFramePointer = reinterpret_cast(__builtin_frame_address(0)); const std::int32_t result = sceFiberRun_nid_postfix(g_first, 100, returned); @@ -319,7 +509,13 @@ void RunInternalOptions() { } -int main() { +int main(int argc, char** argv) { + if (argc == 2 && std::string_view(argv[1]) == "--contextless") { + RunContextlessTransitions(); + RunContextlessOptions(); + return 0; + } + Require(argc == 1, "known test arguments"); RunSmallContextFirst(); FiberObject* self = nullptr; Require(sceFiberGetSelf(&self) == FiberErrorPermission, "no fiber on the thread"); diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index 853a5b5484..fc8af8e721 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -102,6 +103,23 @@ static void CheckExclusiveCppOpen(const std::string& base) { } } +static int APS5_VABI ScanList(FileStream* stream, const char* format, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); +#else + std::va_list args; + va_start(args, format); +#endif + const int result = vfscanf_nid_postfix(stream, format, reinterpret_cast(args)); +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif + return result; +} + static std::string ReadWhole(const std::string& path) { auto* stream = fopen_nid_postfix(path.c_str(), "rb"); Require(stream != nullptr); @@ -158,6 +176,11 @@ static void CheckUpdateStreamReadsAfterWrites(const std::string& base) { Require(fscanf_nid_postfix(stream, "%3s", buffer.data()) == 1, "fscanf after fwrite on an r+ stream"); Require(std::strcmp(buffer.data(), "234") == 0, "fscanf after fwrite on an r+ stream read other bytes"); }); + CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) { + std::array buffer{}; + Require(ScanList(stream, "%3s", buffer.data()) == 1, "vfscanf after fwrite on an r+ stream"); + Require(std::strcmp(buffer.data(), "234") == 0, "vfscanf after fwrite on an r+ stream read other bytes"); + }); CheckReadAfterWrite(path, "w+b", "AB", [](FileStream* stream) { std::array buffer{}; Require(fread_nid_postfix(buffer.data(), 1, buffer.size(), stream) == 0 && feof_nid_postfix(stream) != 0, "fread after fwrite on a w+ stream"); diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index a7de87f988..6b06e1d1dc 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -153,6 +153,53 @@ static bool CheckWidePrecision() { return correct; } +static void CheckIntegerScanWidths() { + int value = 7; + *__error_nid_postfix() = 0; + Require(sscanf_nid_postfix("2147483648", "%d", &value) == 1 && value == -2147483647 - 1 && *__error_nid_postfix() == 0); + Require(sscanf_nid_postfix("0xFFFFFFFF", "%i", &value) == 1 && value == -1 && *__error_nid_postfix() == 0); + Require(sscanf_nid_postfix("99999999999999999999", "%d", &value) == 1 && value == -1 && *__error_nid_postfix() == 34); + unsigned unsignedValue = 7; + Require(sscanf_nid_postfix("4294967296", "%u", &unsignedValue) == 1 && unsignedValue == 0); + Require(sscanf_nid_postfix("100000001", "%x", &unsignedValue) == 1 && unsignedValue == 1); + Require(sscanf_nid_postfix("40000000001", "%o", &unsignedValue) == 1 && unsignedValue == 1); + short shortValue = 7; + Require(sscanf_nid_postfix("5000000000", "%hd", &shortValue) == 1 && shortValue == -3584); + signed char charValue = 7; + Require(sscanf_nid_postfix("300", "%hhd", &charValue) == 1 && charValue == 44); + Require(sscanf_nid_postfix("4294967295", "%u", &unsignedValue) == 1 && unsignedValue == 4294967295u); + unsigned short unsignedShort = 7; + Require(sscanf_nid_postfix("40000", "%hu", &unsignedShort) == 1 && unsignedShort == 40000); + Require(sscanf_nid_postfix("70000", "%hu", &unsignedShort) == 1 && unsignedShort == 4464); + Require(sscanf_nid_postfix("ffff", "%hx", &unsignedShort) == 1 && unsignedShort == 0xffff); + unsigned char unsignedChar = 7; + Require(sscanf_nid_postfix("200", "%hhu", &unsignedChar) == 1 && unsignedChar == 200); + Require(sscanf_nid_postfix("300", "%hhu", &unsignedChar) == 1 && unsignedChar == 44); + Require(sscanf_nid_postfix("377", "%hho", &unsignedChar) == 1 && unsignedChar == 255); + *__error_nid_postfix() = 0; + Require(sscanf_nid_postfix("-2147483649", "%d", &value) == 1 && value == 0x7fffffff && *__error_nid_postfix() == 0); + Require(sscanf_nid_postfix("-1", "%u", &unsignedValue) == 1 && unsignedValue == 0xffffffffu && *__error_nid_postfix() == 0); + Require(sscanf_nid_postfix("99999999999", "%x", &unsignedValue) == 1 && unsignedValue == 0x99999999u && *__error_nid_postfix() == 0); + Require(sscanf_nid_postfix("70000", "%hd", &shortValue) == 1 && shortValue == 0x1170 && *__error_nid_postfix() == 0); + long long quad = 0x5555555555555555ll; + Require(sscanf_nid_postfix("9223372036854775808", "%lld", &quad) == 1 && quad == 0x7fffffffffffffffll && *__error_nid_postfix() == 34); + unsigned long long unsignedQuad = 0x5555555555555555ull; + *__error_nid_postfix() = 0; + Require(sscanf_nid_postfix("18446744073709551616", "%llu", &unsignedQuad) == 1 && unsignedQuad == 0xffffffffffffffffull && *__error_nid_postfix() == 34); +#ifdef _WIN32 + quad = 0x5555555555555555ll; + *__error_nid_postfix() = 0; + Require(sscanf_nid_postfix("5", "%qd", &quad) == 0 && quad == 0x5555555555555555ll && *__error_nid_postfix() == 0); +#endif + int first = 7; + int second = 7; + int position = 0; + Require(sscanf_nid_postfix("12 34", "%d %d%n", &first, &second, &position) == 2 && first == 12 && second == 34 && position == 5); + value = 7; + Require(sscanf_nid_postfix("x", "%d", &value) == 0 && value == 7); + Require(sscanf_nid_postfix("1 x", "%d %d", &first, &value) == 1 && first == 1 && value == 7); +} + static void CheckSecureFormat() { constexpr size_t rsizeMax = SIZE_MAX >> 1; char buffer[16]; @@ -388,6 +435,7 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { Require(ScanListS("123 test", "%d %s", &scanNum, scanWord, 8u) == 2 && scanNum == 123 && std::strcmp(scanWord, "test") == 0); char tooSmall[4] = {'x', 'x', 'x', 'x'}; Require(ScanListS("abcdef", "%s", tooSmall, 4u) == 0 && tooSmall[0] == '\0'); + CheckIntegerScanWidths(); std::puts("Formatting checks passed: 10000 iterations"); } @@ -405,4 +453,113 @@ static void CheckAllocatingFormat() { } -int main() { CheckSizeLimits(); RunChecks(); CheckAllocatingFormat(); return CheckWidePrecision() ? 0 : 1; } +static void RequireFloat(const char* format, double value, const char* expected) { + char buffer[64]; + const int written = snprintf_nid_postfix(buffer, sizeof(buffer), format, value); + if (written != static_cast(std::strlen(expected)) || std::strcmp(buffer, expected) != 0) { + std::fprintf(stderr, "%s of %a gave \"%s\", expected \"%s\"\n", format, value, buffer, expected); + Require(false); + } +} + +static void CheckHexFloat() { + RequireFloat("[%-010a]", 1.5, "[0x1.8p+0 ]"); + RequireFloat("[%-0#12A]", 0.75, "[0X1.8P-1 ]"); + RequireFloat("[%0-10.2a]", 1.0, "[0x1.00p+0 ]"); + RequireFloat("[%010a]", -1.5, "[-0x01.8p+0]"); + RequireFloat("[%a]", 0x1p-1074, "[0x1p-1074]"); + RequireFloat("[%a]", 0x0.fffffffffffffp-1022, "[0x1.ffffffffffffep-1023]"); + RequireFloat("[%.3a]", 0x0.fffffffffffffp-1022, "[0x2.000p-1023]"); + RequireFloat("[%A]", 0x0.012688b70e62bp-1022, "[0X1.2688B70E62BP-1030]"); + RequireFloat("[%.0a]", 0x0.012688b70e62bp-1022, "[0x1p-1030]"); + RequireFloat("[%.1a]", 0x1.08p0, "[0x1.1p+0]"); + RequireFloat("[%.0a]", 0x1.8p0, "[0x2p+0]"); + RequireFloat("[%#.0a]", 1.0, "[0x1.p+0]"); + RequireFloat("[%.20a]", 0.1, "[0x1.999999999999a0000000p-4]"); + RequireFloat("[% a]", -0.0, "[-0x0p+0]"); + char buffer[32]; + Require(snprintf_nid_postfix(buffer, sizeof(buffer), "[%*a]", -12, 1.5) == 14 && std::strcmp(buffer, "[0x1.8p+0 ]") == 0); + Require(snprintf_nid_postfix(buffer, 6, "%a", 1.5) == 8 && std::strcmp(buffer, "0x1.8") == 0); +#ifdef _WIN32 + for (const char* format : {"%.2147483648a", "%.2147483649A", "%.9999999999a", + "%.9999999999999999999999999999999999999999A", "%.0002147483648a", "%-+20.2147483648A", + "%2147483648a", "%999999999999999999999999a", "%.2147483647a", "%.2147483641a", "%+.2147483640A", + "x%2147483647a", "xx%.2147483640a", "x%+.2147483639A"}) { + bool rejected = false; + try { + snprintf_nid_postfix(buffer, sizeof(buffer), format, 1.5); + } catch (const std::overflow_error&) { + rejected = true; + } + Require(rejected); + } +#endif +} + +static void CheckDecimalFloat() { + RequireFloat("[%.0f]", 0.5, "[1]"); + RequireFloat("[%.0f]", 2.5, "[3]"); + RequireFloat("[%.0f]", -0.5, "[-1]"); + RequireFloat("[%.1f]", 0.25, "[0.3]"); + RequireFloat("[%#.3g]", 2025.0, "[2.03e+03]"); + RequireFloat("[%.2e]", 1125.0, "[1.13e+03]"); + RequireFloat("[%.2f]", 0.015, "[0.01]"); + RequireFloat("[%.1f]", 99.95, "[100.0]"); + RequireFloat("[%.40e]", 0.1, "[1.0000000000000000555000000000000000000000e-01]"); + RequireFloat("[%.20f]", 0.1, "[0.10000000000000000555]"); + RequireFloat("[%.0f]", 0x1p100, "[1267650600228229401496700000000]"); + RequireFloat("[%.25e]", 0x1p-1074, "[4.9406564584124654417600000e-324]"); + RequireFloat("[%.23e]", 0x1.0f6b8p+1023, "[9.52987754335065669050000e+307]"); + RequireFloat("[%.17e]", -0x1.16a55a5cd7c73p+48, "[-3.06374418159559187e+14]"); + RequireFloat("[%.17g]", 0x1.e5dec6655f463p+50, "[2136879802973464.7]"); + RequireFloat("[%.0f]", 67108865.5, "[67108865]"); + RequireFloat("[%.0f]", 33554433.5, "[33554434]"); + RequireFloat("[%+08.1f]", 2.25, "[+00002.3]"); + RequireFloat("[%#.0e]", 2.5, "[3.e+00]"); + RequireFloat("[%g]", 0.0001, "[0.0001]"); + RequireFloat("[%G]", 0.00001, "[1E-05]"); + RequireFloat("[%.3g]", 999.5, "[1e+03]"); + RequireFloat("[%e]", 0.0, "[0.000000e+00]"); + RequireFloat("[%#g]", 0.0, "[0.00000]"); + RequireFloat("[%.0f]", -0.0, "[-0]"); + RequireFloat("[%-12.3E]", -1.25e-5, "[-1.250E-05 ]"); + char buffer[64]; + Require(snprintf_nid_postfix(buffer, sizeof(buffer), "[%.40Le]", 1.0L / 3) == 48 && std::strcmp(buffer, "[3.3333333333333333334000000000000000000000e-01]") == 0); + Require(snprintf_nid_postfix(buffer, sizeof(buffer), "[%.2Lf]", 0.125L) == 6 && std::strcmp(buffer, "[0.13]") == 0); + Require(snprintf_nid_postfix(buffer, sizeof(buffer), "[%.30Le]", 0x1p-16445L) == 40 && std::strcmp(buffer, "[3.645199531882474602528000000000e-4951]") == 0); + for (const char* format : {"%.2147483647e", "%+.2147483647E", "%.2147483647f", "% .2147483646F", "%#.2147483647g", + "%.2147483643e", "x%.2147483645f", "x%-2147483647e", "%.2147483647Le"}) { + bool rejected = false; + try { + if (std::strstr(format, "L")) snprintf_nid_postfix(buffer, sizeof(buffer), format, 1.5L); + else snprintf_nid_postfix(buffer, sizeof(buffer), format, 1.5); + } catch (const std::overflow_error&) { + rejected = true; + } + Require(rejected); + } +} + +static void CheckLongDoubleHexFloat() { + constexpr struct { + const char* format; + long double value; + const char* expected; + } cases[] = { + {"[%La]", 0.1L, "[0x1.999999999999999ap-4]"}, {"[%LA]", 1.0L, "[0X1P+0]"}, {"[%.3La]", 0.1L, "[0x1.99ap-4]"}, + {"[%.0La]", 1.5L, "[0x2p+0]"}, {"[%#.0La]", 0.1L, "[0x2.p-4]"}, {"[%La]", 0.0L, "[0x0p+0]"}, {"[%La]", -0.0L, "[-0x0p+0]"}, + {"[%La]", 0x1p-16445L, "[0x1p-16445]"}, {"[%La]", 0x1.fffffffffffffffep16383L, "[0x1.fffffffffffffffep+16383]"}, + {"[%.15La]", 0x1.fffffffffffffffep0L, "[0x2.000000000000000p+0]"}, {"[%-+12.2La]", -2.0L, "[-0x1.00p+1 ]"}, + {"[%010La]", 1.5L, "[0x001.8p+0]"}, {"[%.20La]", 0.1L, "[0x1.999999999999999a0000p-4]"}, + }; + for (const auto& item : cases) { + char buffer[64]; + const int written = snprintf_nid_postfix(buffer, sizeof(buffer), item.format, item.value); + if (written != static_cast(std::strlen(item.expected)) || std::strcmp(buffer, item.expected) != 0) { + std::fprintf(stderr, "%s of %La gave \"%s\", expected \"%s\"\n", item.format, item.value, buffer, item.expected); + Require(false); + } + } +} + +int main() { CheckSizeLimits(); RunChecks(); CheckHexFloat(); CheckDecimalFloat(); CheckLongDoubleHexFloat(); CheckAllocatingFormat(); return CheckWidePrecision() ? 0 : 1; } diff --git a/core/libs/tests/GuestAccess.cpp b/core/libs/tests/GuestAccess.cpp index f7cd52dcae..1c1b6ae651 100644 --- a/core/libs/tests/GuestAccess.cpp +++ b/core/libs/tests/GuestAccess.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/Api.hpp" #include "prx/libc/include/General.hpp" #include +#include #include #include #ifdef _WIN32 @@ -71,6 +72,46 @@ int main() { Require(access_nid_postfix("sample.txt", 1) == -1 && *__error_nid_postfix() == 13); std::filesystem::permissions(file, std::filesystem::perms::owner_exec, std::filesystem::perm_options::add); Require(access_nid_postfix("sample.txt", 1) == 0); +#endif + const auto dangling = directory / "dangling.link"; + const auto pointing = directory / "sample.link"; + const auto loop = directory / "loop.link"; +#ifdef _WIN32 + const DWORD linkFlags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; + const bool linked = CreateSymbolicLinkW(dangling.c_str(), L"missing.bin", linkFlags) && + CreateSymbolicLinkW(pointing.c_str(), L"sample.txt", linkFlags) && CreateSymbolicLinkW(loop.c_str(), L"loop.link", linkFlags); +#else + std::error_code linkError; + std::filesystem::create_symlink("missing.bin", dangling, linkError); + if (!linkError) std::filesystem::create_symlink("sample.txt", pointing, linkError); + if (!linkError) std::filesystem::create_symlink("loop.link", loop, linkError); + const bool linked = !linkError; +#endif + if (linked) { + Require(access_nid_postfix("dangling.link", 0) == -1 && *__error_nid_postfix() == 2); + Require(access_nid_postfix("dangling.link", 4) == -1 && *__error_nid_postfix() == 2); + Require(access_nid_postfix("loop.link", 0) == -1 && *__error_nid_postfix() == 62); + Require(access_nid_postfix("sample.link", 6) == 0); +#ifdef _WIN32 + Require(SetFileAttributesW(file.c_str(), FILE_ATTRIBUTE_READONLY)); + Require(access_nid_postfix("sample.link", 4) == 0); + Require(access_nid_postfix("sample.link", 2) == -1 && *__error_nid_postfix() == 13); + Require(SetFileAttributesW(file.c_str(), FILE_ATTRIBUTE_NORMAL)); + Require(SetFileAttributesW(pointing.c_str(), FILE_ATTRIBUTE_READONLY)); + Require(access_nid_postfix("sample.link", 2) == 0); + Require(SetFileAttributesW(pointing.c_str(), FILE_ATTRIBUTE_NORMAL)); +#endif + } else { + std::fprintf(stderr, "symbolic links unavailable: the access link cases not tested\n"); + } +#ifdef _WIN32 + DeleteFileW(dangling.c_str()); + DeleteFileW(pointing.c_str()); + DeleteFileW(loop.c_str()); +#else + std::filesystem::remove(dangling, linkError); + std::filesystem::remove(pointing, linkError); + std::filesystem::remove(loop, linkError); #endif Require(chdir_nid_postfix("/") == 0); std::filesystem::remove(file); diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index c7780f5f65..1aece55d9e 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -825,7 +825,7 @@ void TestGetInfo(std::uint32_t context) { Require(sceAjmBatchJobGetInfo(&info, instance, &at9) == 0); Submit(context, info); Require(initResult[0] == 0); - Require(at9.result == 0 && at9.internalResult == 0 && at9.numChannels == 2 && at9.channelMask == 0x3 && at9.sampleRate == 48000 && at9.sampleEncoding == 0 && at9.reserved == 0); + Require(at9.result == 0 && at9.internalResult == 0 && at9.numChannels == 2 && at9.channelMask == 0x3 && at9.sampleRate == 48000 && at9.sampleEncoding == 0 && at9.bitrate == 1024 * 8 * 48000 / (4 * 256) && at9.reserved == 0); Require(sceAjmInstanceDestroy(context, instance) == 0); Require(sceAjmInstanceCreate(context, 0, 0, &instance) == 0); @@ -841,6 +841,69 @@ void TestGetInfo(std::uint32_t context) { Require(sceAjmInstanceDestroy(context, instance) == 0); } +void TestBatchBounds(std::uint32_t context) { + constexpr int invalidParameter = static_cast(0x80930005); + std::vector storage(128); + AjmBatchInfo info{}; + Require(sceAjmBatchInitialize(storage.data(), storage.size(), &info) == 0); + const std::uint32_t parameters[3] = {2, 48000, 0}; + std::int32_t result[2] = {-1, -1}; + Require(sceAjmBatchJobInitialize(&info, 0, parameters, sizeof(parameters), result) == 0); + const auto completeBytes = info.offset; +#ifdef _WIN32 + SYSTEM_INFO system{}; + GetSystemInfo(&system); + const std::size_t pageSize = system.dwPageSize; + auto* pages = static_cast(VirtualAlloc(nullptr, 2 * pageSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(pages != nullptr); + DWORD oldProtection = 0; + Require(VirtualProtect(pages + pageSize, pageSize, PAGE_NOACCESS, &oldProtection) != 0); +#else + const long hostPageSize = sysconf(_SC_PAGESIZE); + Require(hostPageSize > 0); + const std::size_t pageSize = static_cast(hostPageSize); + auto* pages = static_cast(mmap(nullptr, 2 * pageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(pages != MAP_FAILED); + Require(mprotect(pages + pageSize, pageSize, PROT_NONE) == 0); +#endif + Require(completeBytes < pageSize); + for (std::size_t bytes = 1; bytes < completeBytes; ++bytes) { + auto* truncated = pages + pageSize - bytes; + std::memcpy(truncated, storage.data(), bytes); + AjmBatchInfo partial{truncated, bytes, bytes}; + std::uint32_t batch = 0xdeadbeef; + bool threw = false; + try { + sceAjmBatchStart(context, &partial, 0, nullptr, &batch); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && batch == 0xdeadbeef && result[0] == -1 && result[1] == -1); + } + for (std::uint8_t value : {std::uint8_t{0}, std::uint8_t{0xff}}) { + std::fill(storage.begin(), storage.end(), value); + std::uint32_t batch = 0xdeadbeef; + bool threw = false; + try { + sceAjmBatchStart(context, &info, 0, nullptr, &batch); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && batch == 0xdeadbeef && result[0] == -1 && result[1] == -1); + } + std::uint32_t batch = 0xdeadbeef; + AjmBatchInfo invalid{pages + pageSize, 1, 0}; + Require(sceAjmBatchStart(context, &invalid, 0, nullptr, &batch) == invalidParameter); + invalid = {nullptr, 1, 1}; + Require(sceAjmBatchStart(context, &invalid, 0, nullptr, &batch) == invalidParameter); + Require(batch == 0xdeadbeef); +#ifdef _WIN32 + Require(VirtualFree(pages, 0, MEM_RELEASE) != 0); +#else + Require(munmap(pages, 2 * pageSize) == 0); +#endif +} + void TestBatchWaitRelease(std::uint32_t context) { constexpr int invalidBatch = static_cast(0x80930004); std::vector batch(64); @@ -988,7 +1051,7 @@ std::vector DecodeAt9Superframe(std::uint32_t context, const std:: Require(sceAjmBatchJobRun(&info, instance, sidebandStream | runMultipleFrames, superframe.data(), superframe.size(), pcm.data(), pcm.size() * sizeof(std::int16_t), &decoded, sizeof(decoded)) == 0); Submit(context, info); Require(initResult[0] == 0); - Require(decoded.stream.result == 0 && static_cast(decoded.stream.inputConsumed) == superframe.size()); + Require(decoded.stream.result == 0x18 && static_cast(decoded.stream.inputConsumed) == superframe.size()); Require(static_cast(decoded.stream.outputWritten) == pcm.size() * sizeof(std::int16_t) && decoded.stream.totalDecodedSamples == 1024 && decoded.frames == 4); Require(sceAjmInstanceDestroy(context, instance) == 0); return pcm; @@ -1515,7 +1578,7 @@ ResampledStream Stream(std::uint32_t context, std::uint32_t instance, const std: Require(sceAjmBatchJobDecode(&info, instance, stream + offset, size - offset, pcm.data(), pcm.size() * sizeof(std::int16_t), &sideband) == 0); Submit(context, info); if (ratio > 0) Require(setResult[0] == 0); - Require(sideband.result == 0 || (offset == size && sideband.outputWritten == 0)); + Require(sideband.result == 0 || sideband.result == 0x8 || sideband.result == 0x10 || sideband.result == 0x18 || (offset == size && sideband.outputWritten == 0)); offset += static_cast(sideband.inputConsumed); if (offset < size && static_cast(sideband.outputWritten) < pcm.size() * sizeof(std::int16_t)) ++out.shortJobs; out.pcm.insert(out.pcm.end(), pcm.begin(), pcm.begin() + sideband.outputWritten / static_cast(sizeof(std::int16_t))); @@ -1735,7 +1798,7 @@ At9Run RunAt9Job(std::uint32_t context, std::uint32_t instance, std::uint64_t fl const std::size_t sidebandSize = (flags & runMultipleFrames) != 0 ? sizeof(decoded) : sizeof(decoded.stream); Require(sceAjmBatchJobRun(&info, instance, sidebandStream | flags, input, inputSize, pcm.data(), pcm.size() * sizeof(std::int16_t), &decoded, sidebandSize) == 0); Submit(context, info); - Require(decoded.stream.result == 0); + Require(decoded.stream.result == 0 || decoded.stream.result == 0x8 || decoded.stream.result == 0x18); return decoded; } @@ -1769,7 +1832,318 @@ void TestAt9RunDecodesWholeInput(std::uint32_t context) { RunControl(context, instance, CONTROL_START, &start, sizeof(start), result); Require(result[0] == 0); const auto run = RunAt9Job(context, instance, 0, stream.data(), stream.size()); - Require(static_cast(run.stream.inputConsumed) == stream.size() && run.stream.outputWritten == 1024 * 2); + Require(run.stream.result == 0x18 && static_cast(run.stream.inputConsumed) == stream.size() && run.stream.outputWritten == 1024 * 2); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +void TestAt9FlagRules(std::uint32_t context) { + constexpr std::uint64_t runMultipleFrames = 1ull << 12; + constexpr std::uint64_t sidebandStream = 1ull << 47; + std::vector stream(2048, 0); + std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); + std::memcpy(stream.data() + 1024, AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); + const At9Control start{{0, 0, 0}, {0xFE, 0x70, 0x1F, 0xF0}, 0}; + const auto run = [&](std::size_t inputSize, std::size_t roomSamples, At9Run& decoded) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + std::int32_t result[2] = {-1, -1}; + RunControl(context, instance, CONTROL_START, &start, sizeof(start), result); + Require(result[0] == 0); + std::vector batch(4096); + std::vector pcm(roomSamples + 1); + AjmBatchInfo info{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobRun(&info, instance, sidebandStream | runMultipleFrames, stream.data(), inputSize, pcm.data(), roomSamples * sizeof(std::int16_t), &decoded, sizeof(decoded)) == 0); + Submit(context, info); + Require(sceAjmInstanceDestroy(context, instance) == 0); + }; + At9Run inputRanOut{{-1, -1, 0, 0, 0}, 0, 0}; + run(1024, 2048, inputRanOut); + Require(inputRanOut.stream.result == 0x8 && inputRanOut.stream.inputConsumed == 1024 && inputRanOut.stream.outputWritten == 1024 * 2 && inputRanOut.frames == 4); + At9Run roomRanOut{{-1, -1, 0, 0, 0}, 0, 0}; + run(2048, 1024, roomRanOut); + Require(roomRanOut.stream.result == 0x10 && roomRanOut.stream.inputConsumed == 1024 && roomRanOut.stream.outputWritten == 1024 * 2 && roomRanOut.frames == 4); + At9Run halfRoom{{-1, -1, 0, 0, 0}, 0, 0}; + run(1024, 512, halfRoom); + Require(halfRoom.stream.result == 0x10 && halfRoom.stream.inputConsumed > 0 && halfRoom.stream.inputConsumed < 1024 && halfRoom.stream.outputWritten == 512 * 2 && halfRoom.frames == 2); + At9Run noRoom{{-1, -1, 0, 0, 0}, 0, 0}; + run(1024, 0, noRoom); + Require(noRoom.stream.result == 0x10 && noRoom.stream.inputConsumed == 0 && noRoom.stream.outputWritten == 0 && noRoom.frames == 0); + At9Run both{{-1, -1, 0, 0, 0}, 0, 0}; + run(1024, 1024, both); + Require(both.stream.result == 0x18 && both.stream.inputConsumed == 1024 && both.stream.outputWritten == 1024 * 2 && both.frames == 4); +} + +int StartBuffer(std::uint32_t context, const void* batch, std::size_t size) { + std::uint32_t id = 0; + AjmBatchError error{}; + const int started = sceAjmBatchStartBuffer(context, static_cast(batch), static_cast(size), 0, &error, &id); + if (started == 0) Require(sceAjmBatchWait(context, id, 0, &error) == 0); + return started; +} + +std::uint32_t BatchWord(const std::vector& batch, std::size_t offset) { + std::uint32_t word = 0; + std::memcpy(&word, batch.data() + offset, sizeof(word)); + return word; +} + +void TestConsoleBufferJobs(std::uint32_t context) { + constexpr std::uint64_t version = 1; + constexpr std::uint64_t runMultipleFrames = 1ull << 12; + constexpr std::uint64_t sidebandStream = 1ull << 47; + std::vector stream(2048, 0); + std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); + std::memcpy(stream.data() + 1024, AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + const At9Control start{{0, 0, 0}, {0xFE, 0x70, 0x1F, 0xF0}, 0}; + std::int32_t result[2] = {-1, -1}; + std::vector pcm(2048, 0x55); + At9Run single{{-1, -1, 0, 0, 0}, 0, 0}; + At9Run split{{-1, -1, 0, 0, 0}, 0, 0}; + const AjmBuffer inputs[2] = {{stream.data() + 1024, 300}, {stream.data() + 1324, 724}}; + const AjmBuffer outputs[2] = {{pcm.data() + 1024, 600 * 2}, {pcm.data() + 1624, 424 * 2}}; + std::vector batch(4096, 0xCD); + + void* at = sceAjmBatchJobControlBufferRa(batch.data(), instance, version | CONTROL_START, &start, sizeof(start), result, sizeof(result), &context); + Require(at == batch.data() + 64); + Require(BatchWord(batch, 0) == instance << 6 && BatchWord(batch, 4) == 56); + Require(BatchWord(batch, 8) == 6 && BatchWord(batch, 24) == 2 && BatchWord(batch, 28) == sizeof(start)); + Require(BatchWord(batch, 40) == (3u | (0x2000u << 6)) && BatchWord(batch, 44) == 0x6001 && BatchWord(batch, 48) == 18); + at = sceAjmBatchJobRunBufferRa(at, instance, version | sidebandStream | runMultipleFrames, stream.data(), 1024, pcm.data(), 1024 * 2, &single, sizeof(single), nullptr); + Require(at == batch.data() + 128); + Require(BatchWord(batch, 68) == 56 && BatchWord(batch, 72) == 1 && BatchWord(batch, 88) == (4u | (0x8000u << 6)) && BatchWord(batch, 96) == 17 && BatchWord(batch, 112) == 18); + at = sceAjmBatchJobRunSplitBufferRa(at, instance, version | sidebandStream | runMultipleFrames, inputs, 2, outputs, 2, &split, sizeof(split), nullptr); + Require(at == batch.data() + 224 && BatchWord(batch, 132) == 88); + Require(StartBuffer(context, batch.data(), static_cast(at) - batch.data()) == 0); + + Require(result[0] == 0 && result[1] == 0); + constexpr std::int32_t exactFit = 0x18; + Require(single.stream.result == exactFit && single.stream.inputConsumed == 1024 && single.stream.outputWritten == 1024 * 2 && single.frames == 4 && single.stream.totalDecodedSamples == 1024); + Require(split.stream.result == exactFit && split.stream.inputConsumed == 1024 && split.stream.outputWritten == 1024 * 2 && split.frames == 4 && split.stream.totalDecodedSamples == 2048); + Require(std::all_of(pcm.begin(), pcm.end(), [](std::int16_t sample) { return sample == 0; })); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +class ConsoleBatch { +public: + void Word(std::uint32_t word) { words.push_back(word); } + void Chunk(std::uint32_t kind, std::uint32_t payload, std::uint32_t word) { + Word(kind | (payload << 6)); + Word(word); + } + void Buffer(std::uint32_t kind, const void* address, std::size_t size) { + Chunk(kind, 0, static_cast(size)); + const auto value = static_cast(reinterpret_cast(address)); + Word(static_cast(value)); + Word(static_cast(value >> 32)); + } + const std::uint32_t* Data() const { return words.data(); } + std::size_t Bytes() const { return words.size() * sizeof(std::uint32_t); } + +private: + std::vector words; +}; + +void TestConsoleHandWrittenJobs(std::uint32_t context) { + std::vector stream(1024, 0); + std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + std::int32_t result[2] = {-1, -1}; + std::int32_t statistics[6] = {-1, -1, -1, -1, -1, -1}; + std::vector pcm(1024, 0x55); + At9Run decoded{{-1, -1, 0, 0, 0}, 0, 0}; + const std::uint8_t initialize[8] = {0xFE, 0x70, 0x1F, 0xF0, 0, 0, 0, 0}; + std::uint32_t inlined[2]; + std::memcpy(inlined, initialize, sizeof(initialize)); + + ConsoleBatch batch; + batch.Chunk(7, 0, sizeof(initialize)); + batch.Word(inlined[0]); + batch.Word(inlined[1]); + batch.Chunk(0, instance, 40); + batch.Buffer(2, nullptr, sizeof(initialize)); + batch.Chunk(3, 0, 0x6001); + batch.Buffer(18, result, sizeof(result)); + batch.Chunk(0, instance, 56); + batch.Buffer(1, stream.data(), stream.size()); + batch.Chunk(4, 0x8000, 0x1001); + batch.Buffer(17, pcm.data(), pcm.size() * sizeof(std::int16_t)); + batch.Buffer(18, &decoded, sizeof(decoded)); + batch.Chunk(0, 0x80000, 24); + batch.Chunk(3, 0, 0x8007); + batch.Buffer(18, statistics, sizeof(statistics)); + std::vector words(batch.Data(), batch.Data() + batch.Bytes() / sizeof(std::uint32_t)); + const auto inlineAddress = static_cast(reinterpret_cast(words.data() + 2)); + words[8] = static_cast(inlineAddress); + words[9] = static_cast(inlineAddress >> 32); + Require(StartBuffer(context, words.data(), words.size() * sizeof(std::uint32_t)) == 0); + Require(result[0] == 0 && result[1] == 0 && statistics[0] == 0 && statistics[1] == 0); + Require(decoded.stream.result == 0x18 && decoded.stream.inputConsumed == 1024 && decoded.stream.outputWritten == 1024 * 2 && decoded.frames == 4); + Require(std::all_of(pcm.begin(), pcm.end(), [](std::int16_t sample) { return sample == 0; })); + + constexpr int malformedBatch = static_cast(0x80930011); + Require(StartBuffer(context, words.data(), 12) == malformedBatch); + ConsoleBatch flagless; + flagless.Chunk(0, instance, 16); + flagless.Buffer(18, result, sizeof(result)); + Require(StartBuffer(context, flagless.Data(), flagless.Bytes()) == malformedBatch); + ConsoleBatch overrun; + overrun.Chunk(0, instance, 32); + overrun.Chunk(3, 0, 0x2001); + overrun.Buffer(18, result, sizeof(result)); + Require(StartBuffer(context, overrun.Data(), overrun.Bytes()) == malformedBatch); + ConsoleBatch unknown; + unknown.Chunk(0, instance, 16); + unknown.Chunk(3, 0, 0x2001); + unknown.Chunk(5, 0, 0); + bool unknownThrows = false; + try { + static_cast(StartBuffer(context, unknown.Data(), unknown.Bytes())); + } catch (const std::runtime_error&) { + unknownThrows = true; + } + Require(unknownThrows); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +int StartBufferRefused(std::uint32_t context, const void* batch, std::size_t size, AjmBatchError& error) { + std::uint32_t id = 0; + error = AjmBatchError{}; + return sceAjmBatchStartBuffer(context, static_cast(batch), static_cast(size), 0, &error, &id); +} + +void TestConsoleBufferEdges(std::uint32_t context) { + constexpr int malformedBatch = static_cast(0x80930011); + constexpr int invalidAddress = static_cast(0x80930016); + std::vector stream(1024, 0); + std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); + const std::uint8_t initialize[8] = {0xFE, 0x70, 0x1F, 0xF0, 0, 0, 0, 0}; + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + std::vector pcm(1024, 0x55); + std::int32_t result[2] = {-1, -1}; + const auto control = [&](ConsoleBatch& batch) { + result[0] = result[1] = -1; + batch.Chunk(0, instance, 40); + batch.Buffer(2, initialize, sizeof(initialize)); + batch.Chunk(3, 0, 0x6001); + batch.Buffer(18, result, sizeof(result)); + }; + + At9Run dropped{{-1, -1, 0, 0, 0}, 0, 0}; + ConsoleBatch zeroSizes; + control(zeroSizes); + zeroSizes.Chunk(0, instance, 104); + zeroSizes.Buffer(1, stream.data(), 0); + zeroSizes.Buffer(1, stream.data(), stream.size()); + zeroSizes.Chunk(4, 0x8000, 0x1001); + zeroSizes.Buffer(17, pcm.data(), 0); + zeroSizes.Buffer(17, pcm.data(), pcm.size() * sizeof(std::int16_t)); + zeroSizes.Buffer(17, nullptr, 0); + zeroSizes.Buffer(18, &dropped, sizeof(dropped)); + Require(StartBuffer(context, zeroSizes.Data(), zeroSizes.Bytes()) == 0); + Require(result[0] == 0 && dropped.stream.result == 0x18 && dropped.stream.inputConsumed == 1024 && dropped.stream.outputWritten == 1024 * 2 && dropped.frames == 4); + Require(std::all_of(pcm.begin(), pcm.end(), [](std::int16_t sample) { return sample == 0; })); + + At9Run noRoom{{-1, -1, 0, 0, 0}, 0, 0}; + std::fill(pcm.begin(), pcm.end(), std::int16_t{0x55}); + ConsoleBatch zeroRoom; + control(zeroRoom); + zeroRoom.Chunk(0, instance, 56); + zeroRoom.Buffer(1, stream.data(), stream.size()); + zeroRoom.Chunk(4, 0x8000, 0x1001); + zeroRoom.Buffer(17, pcm.data(), 0); + zeroRoom.Buffer(18, &noRoom, sizeof(noRoom)); + Require(StartBuffer(context, zeroRoom.Data(), zeroRoom.Bytes()) == 0); + Require(result[0] == 0 && noRoom.stream.result == 0x10 && noRoom.stream.inputConsumed == 0 && noRoom.stream.outputWritten == 0 && noRoom.frames == 0); + Require(std::all_of(pcm.begin(), pcm.end(), [](std::int16_t sample) { return sample == 0x55; })); + + const void* returnAddress = &stream; + for (const bool nullInput : {true, false}) { + At9Run untouched{{-1, -1, 0, 0, 0}, 0, 0}; + ConsoleBatch refused; + control(refused); + refused.Chunk(0, instance, 72); + refused.Buffer(6, returnAddress, 0); + refused.Buffer(1, nullInput ? nullptr : stream.data(), stream.size()); + refused.Chunk(4, 0x8000, 0x1001); + refused.Buffer(17, nullInput ? pcm.data() : nullptr, pcm.size() * sizeof(std::int16_t)); + refused.Buffer(18, &untouched, sizeof(untouched)); + AjmBatchError error{}; + Require(StartBufferRefused(context, refused.Data(), refused.Bytes(), error) == malformedBatch); + Require(error.error_code == invalidAddress && error.job_addr == reinterpret_cast(refused.Data()) + 48); + Require(error.cmd_offset == (nullInput ? 24u : 48u) && error.job_ra == returnAddress); + Require(result[0] == -1 && untouched.stream.result == -1); + } + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +void TestConsoleEmptyBatch(std::uint32_t context) { + constexpr int invalidParameter = static_cast(0x80930005); + constexpr int invalidBatch = static_cast(0x80930004); + std::uint32_t id = 0; + AjmBatchError error{}; + Require(sceAjmBatchStartBuffer(context, nullptr, 0, 0, &error, &id) == invalidParameter); + Require(sceAjmBatchStartBuffer(context, nullptr, 16, 0, &error, &id) == invalidParameter); + const std::uint64_t words[2] = {0, 0}; + Require(sceAjmBatchStartBuffer(context, reinterpret_cast(words), 0, 0, &error, nullptr) == invalidParameter); + error.error_code = -1; + Require(sceAjmBatchStartBuffer(context, reinterpret_cast(words), 0, 0, &error, &id) == 0); + Require(error.error_code == 0 && id != 0 && sceAjmBatchWait(context, id, 0, &error) == 0); + Require(sceAjmBatchWait(context, id, 0, &error) == invalidBatch); +} + +void TestConsoleSingleFrameRun(std::uint32_t context) { + constexpr std::uint64_t version = 1; + constexpr std::uint64_t sidebandStream = 1ull << 47; + std::vector stream(1024, 0); + std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + const At9Control start{{0, 0, 0}, {0xFE, 0x70, 0x1F, 0xF0}, 0}; + std::int32_t result[2] = {-1, -1}; + std::vector pcm(1024, 0x55); + DecodeSideband single{-1, -1, 0, 0, 0}; + std::vector batch(4096, 0xCD); + void* at = sceAjmBatchJobControlBufferRa(batch.data(), instance, version | CONTROL_START, &start, sizeof(start), result, sizeof(result), nullptr); + at = sceAjmBatchJobRunBufferRa(at, instance, version | sidebandStream, stream.data(), stream.size(), pcm.data(), pcm.size() * sizeof(std::int16_t), &single, sizeof(single), nullptr); + Require(StartBuffer(context, batch.data(), static_cast(at) - batch.data()) == 0); + Require(result[0] == 0 && single.result == 0 && single.inputConsumed > 0 && single.inputConsumed < 1024); + Require(single.outputWritten == 256 * 2 && single.totalDecodedSamples == 256); + Require(std::all_of(pcm.begin(), pcm.begin() + 256, [](std::int16_t sample) { return sample == 0; })); + Require(std::all_of(pcm.begin() + 256, pcm.end(), [](std::int16_t sample) { return sample == 0x55; })); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +void TestConsoleStaleHeaderBits(std::uint32_t context) { + std::vector stream(1024, 0); + std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + std::int32_t result[2] = {-1, -1}; + std::vector pcm(1024, 0x55); + At9Run decoded{{-1, -1, 0, 0, 0}, 0, 0}; + const std::uint8_t initialize[8] = {0xFE, 0x70, 0x1F, 0xF0, 0, 0, 0, 0}; + + ConsoleBatch batch; + batch.Chunk(0x8C000030u, instance, 56); + batch.Buffer(0xFFFFFFF6u, &context, 0); + batch.Buffer(0xFFFFFFE2u, initialize, sizeof(initialize)); + batch.Chunk(0x04000033u, 0, 0x6001); + batch.Buffer(0xFFFFFFF2u, result, sizeof(result)); + batch.Chunk(0x8C000020u, instance, 72); + batch.Buffer(0xFFFFFFF6u, &context, 0); + batch.Buffer(0xFFFFFFE1u, stream.data(), stream.size()); + batch.Chunk(0x04000034u, 0x8000, 0x1001); + batch.Buffer(0xFFFFFFF1u, pcm.data(), pcm.size() * sizeof(std::int16_t)); + batch.Buffer(0xFFFFFFF2u, &decoded, sizeof(decoded)); + Require(StartBuffer(context, batch.Data(), batch.Bytes()) == 0); + Require(result[0] == 0 && result[1] == 0); + Require(decoded.stream.result == 0x18 && decoded.stream.inputConsumed == 1024 && decoded.stream.outputWritten == 1024 * 2 && decoded.frames == 4); + Require(std::all_of(pcm.begin(), pcm.end(), [](std::int16_t sample) { return sample == 0; })); Require(sceAjmInstanceDestroy(context, instance) == 0); } @@ -2003,6 +2377,7 @@ int main() { TestGaplessDecode(context); TestCodecInfo(context); TestGetInfo(context); + TestBatchBounds(context); TestBatchWaitRelease(context); TestInstanceHandleLifetime(context); TestBatchCancel(context); @@ -2024,6 +2399,14 @@ int main() { TestResampleAt9(context); TestAt9GaplessSegments(context); TestAt9RunDecodesWholeInput(context); + TestAt9FlagRules(context); TestPcmResample(context); + TestConsoleBufferJobs(context); + TestConsoleHandWrittenJobs(context); + TestConsoleBufferEdges(context); + TestConsoleEmptyBatch(context); + TestConsoleSingleFrameRun(context); + TestConsoleStaleHeaderBits(context); + Require(sceAjmDecWVorbisCreateHeaderPacket(nullptr) == static_cast(0x8A6E0001)); Require(sceAjmFinalize(context) == 0); } diff --git a/core/libs/tests/GuestAjmVibration.cpp b/core/libs/tests/GuestAjmVibration.cpp index b130f5fbd2..8157596435 100644 --- a/core/libs/tests/GuestAjmVibration.cpp +++ b/core/libs/tests/GuestAjmVibration.cpp @@ -98,7 +98,7 @@ std::vector Decode(std::uint32_t context, unsigned channels, unsigned encodin Require((stream.result & 2) != 0 && stream.consumed == 0 && stream.produced == 0, "invalid first-frame flag"); } const auto stream = run(packet, packet.size()); - Require(stream.result == 0 && stream.internalResult == 0, "decode vibration superframe"); + Require(stream.result == 0x18 && stream.internalResult == 0, "decode vibration superframe"); Require(stream.consumed == 128 && stream.produced == static_cast(pcm.size() * sizeof(T)), "consumed/produced accounting"); Require(stream.samples == static_cast(samples) && stream.frames == 4, "decoded sample/frame accounting"); Require(sceAjmInstanceDestroy(context, instance) == 0, "destroy instance"); diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index 876515f966..e1197f4a94 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -27,7 +27,11 @@ int main() { Require(sceAppContentAddcontUnmount(nullptr) == notInitialized); Require(sceAppContentGetAddcontInfo(0, nullptr, nullptr) == notInitialized); Require(sceAppContentGetAddcontInfoList(0, nullptr, 0, nullptr) == notInitialized); + Require(sceAppContentGetEntitlementKey(0, nullptr, nullptr) == notInitialized); Require(sceAppContentAppParamGetInt(0, nullptr) == notInitialized); + AppContentMountPoint uninitializedMount{}; + Require(sceAppContentTemporaryDataMount(nullptr) == notInitialized); + Require(sceAppContentTemporaryDataMount(&uninitializedMount) == notInitialized); Require(sceAppContentTemporaryDataMount2(0, nullptr) == notInitialized); Require(sceAppContentTemporaryDataUnmount(nullptr) == notInitialized); Require(sceAppContentTemporaryDataFormat(nullptr) == notInitialized); @@ -192,6 +196,16 @@ int main() { Require(std::memcmp(info, untouchedInfo, sizeof(info)) == 0); Require(sceAppContentGetAddcontInfo(0, nullptr, reinterpret_cast(info)) == ErrorParameter); Require(sceAppContentGetAddcontInfo(0, &label, nullptr) == ErrorParameter); + AppContentEntitlementKey key; + std::memset(&key, 0x5a, sizeof(key)); + const AppContentEntitlementKey untouchedKey = key; + for (const std::uint32_t serviceLabel : {0u, 7u, 8u, 0xffffffffu}) { + Require(sceAppContentGetEntitlementKey(serviceLabel, &label, &key) == ErrorDrmNoEntitlement); + Require(std::memcmp(&key, &untouchedKey, sizeof(key)) == 0); + } + Require(sceAppContentGetEntitlementKey(0, nullptr, &key) == ErrorParameter); + Require(std::memcmp(&key, &untouchedKey, sizeof(key)) == 0); + Require(sceAppContentGetEntitlementKey(0, &label, nullptr) == ErrorParameter); for (const std::uint32_t version : {0x014fffffu, 0x01500000u, 0x034fffffu, 0x03500000u}) { std::memcpy(process.data() + 16, &version, sizeof(version)); @@ -202,6 +216,8 @@ int main() { const int lateError = version >= 0x03500000 ? ErrorParameter : ErrorNotFound; Require(sceAppContentAddcontMount(0, &label, &mountPoint) == earlyError); Require(sceAppContentGetAddcontInfo(0, &label, reinterpret_cast(info)) == infoError); + Require(sceAppContentGetEntitlementKey(0, &label, &key) == infoError); + Require(std::memcmp(&key, &untouchedKey, sizeof(key)) == 0); Require(sceAppContentAddcontEnqueueDownload(0, &label) == earlyError); Require(sceAppContentAddcontDelete(0, &label) == lateError); Require(sceAppContentGetAddcontDownloadProgress(0, &label, progress) == lateError); diff --git a/core/libs/tests/GuestAudio3dMixer.cpp b/core/libs/tests/GuestAudio3dMixer.cpp index c7b09af09c..3e48e963fd 100644 --- a/core/libs/tests/GuestAudio3dMixer.cpp +++ b/core/libs/tests/GuestAudio3dMixer.cpp @@ -51,6 +51,13 @@ int main() { Audio3dInternal::MixFrame(loud_sources, 2, 1, loud_out); Require(loud_out.size() == 2 && loud_out[0] == 32767 && loud_out[1] == 32767, "sums above full scale clamp"); + std::int16_t loud_neg[1] = {-32768}; + Audio3dInternal::Source loud_neg_source{loud_neg, 1, 1, false, 1.0f, 0.0f}; + Audio3dInternal::Source loud_neg_sources[2] = {loud_neg_source, loud_neg_source}; + std::vector loud_neg_out; + Audio3dInternal::MixFrame(loud_neg_sources, 2, 1, loud_neg_out); + Require(loud_neg_out.size() == 2 && loud_neg_out[0] == -32767 && loud_neg_out[1] == -32767, "sums below negative full scale clamp"); + Audio3dInternal::Source out_of_range{panned, 1, 2, false, 1.0f, 2.0f}; std::vector out_of_range_out; Audio3dInternal::MixFrame(&out_of_range, 1, 1, out_of_range_out); diff --git a/core/libs/tests/GuestAudio3dPort.cpp b/core/libs/tests/GuestAudio3dPort.cpp index 5833c4a051..b41d7c686a 100644 --- a/core/libs/tests/GuestAudio3dPort.cpp +++ b/core/libs/tests/GuestAudio3dPort.cpp @@ -189,6 +189,8 @@ void CheckQueue() { Require(sceAudio3dPortGetQueueLevel(0, nullptr, nullptr) == INVALID_PARAMETER, "both outputs null"); std::uint32_t available = 0; Require(sceAudio3dPortGetQueueLevel(0, nullptr, &available) == 0 && available == 2, "level pointer is optional"); + std::uint32_t level = 99; + Require(sceAudio3dPortGetQueueLevel(0, &level, nullptr) == 0 && level == 0, "available pointer is optional"); RequireLevel(0, 2, "empty queue"); Require(sceAudio3dPortPush(0, 1) == 0, "push of an empty queue"); RequireLevel(0, 2, "empty push queues nothing"); diff --git a/core/libs/tests/GuestAudio3dSpeakerArray.cpp b/core/libs/tests/GuestAudio3dSpeakerArray.cpp index 4a70481074..796ffaa6e7 100644 --- a/core/libs/tests/GuestAudio3dSpeakerArray.cpp +++ b/core/libs/tests/GuestAudio3dSpeakerArray.cpp @@ -55,6 +55,8 @@ int main() { Require(coefficients2[3] == 0.0f, "mix coefficients 2 are written"); RequireThrows([&] { sceAudio3dGetSpeakerArrayMixCoefficients(handle, AudioOut2Position{1, 0, 0}, 0.0f, coefficients, 3); }, "a coefficient count that differs from the array is rejected"); RequireThrows([&] { sceAudio3dGetSpeakerArrayMixCoefficients(handle, AudioOut2Position{1, 0, 0}, 0.0f, nullptr, 4); }, "null coefficients pointer throws invalid_argument"); + RequireThrows([&] { sceAudio3dGetSpeakerArrayMixCoefficients2(handle, AudioOut2Position{1, 0, 0}, 0.0f, coefficients, 3, 1, 0.5f); }, "a coefficient count that differs from the array is rejected in v2"); + RequireThrows([&] { sceAudio3dGetSpeakerArrayMixCoefficients2(handle, AudioOut2Position{1, 0, 0}, 0.0f, nullptr, 4, 1, 0.5f); }, "null coefficients pointer throws invalid_argument in v2"); Require(sceAudio3dDeleteSpeakerArray(handle) == 0, "delete succeeds"); RequireThrows([&] { sceAudio3dDeleteSpeakerArray(handle); }, "a deleted array is no longer valid"); diff --git a/core/libs/tests/GuestAudioPropagation.cpp b/core/libs/tests/GuestAudioPropagation.cpp index b1435209ae..7c3df84185 100644 --- a/core/libs/tests/GuestAudioPropagation.cpp +++ b/core/libs/tests/GuestAudioPropagation.cpp @@ -226,7 +226,6 @@ int main() { Require(sceAudioPropagationSourceCalculateAudioPaths(source, rays, 0, 1, rays, 6) == AudioPropagation::ErrorInvalidArgument, "path calculation empty rays"); Require(sceAudioPropagationSourceCalculateAudioPaths(source, rays, 6, 0, rays, 6) == AudioPropagation::ErrorInvalidArgument, "path calculation zero reflection order"); Require(sceAudioPropagationSourceCalculateAudioPaths(source, rays, 6, 1, rays, 0) == AudioPropagation::ErrorInvalidArgument, "path calculation zero output capacity"); - RequireThrows([&] { sceAudioPropagationSourceCalculateAudioPaths(source, rays, 6, 1, rays, 6); }, "unsupported path calculation"); Require(sceAudioPropagationSourceSetAudioPaths(source, nullptr, 0) == 0, "empty audio paths failed"); RequireThrows([&] { sceAudioPropagationSourceSetAudioPaths(source, rays, 1); }, "set audio paths"); diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index a784e37263..6b757d9121 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -806,6 +806,8 @@ void TestPlayback() { Check(sceAvPlayerPostInit(player, &post) == 0, "post init failed"); Check(sceAvPlayerPostInit(player, nullptr) == InvalidParams, "post init accepted null data"); Check(sceAvPlayerStreamCount(player) == OperationFailed, "stream count without a source"); + Check(sceAvPlayerSetTrickSpeed(player, 100) == OperationFailed, "trick speed accepted without a source"); + Check(sceAvPlayerSetTrickSpeed(player, 0) == OperationFailed, "trick speed checked the speed before the source"); Check(!sceAvPlayerIsActive(player), "active without a source"); Check(sceAvPlayerAddSource(player, "/app0/missing.mp4") == OperationFailed, "missing file accepted"); Check(sceAvPlayerAddSource(player, "/app0/missing/../avplayer.mp4") == OperationFailed, "missing parent accepted"); @@ -1041,6 +1043,7 @@ void TestExtendedExports() { Check(sceAvPlayerSetAvailableBandwidth(player, 4000, 1000, 8000) == 0, "bandwidth rejected"); Check(sceAvPlayerSetAvailableBandwidth(nullptr, 4000, 1000, 8000) == InvalidParams, "bandwidth accepted for a null player"); Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "add source failed"); + Check(sceAvPlayerSetAvailableBandwidth(player, 4000, 1000, 8000) == OperationFailed, "bandwidth accepted after a source"); Check(WaitFor([&] { return events.Seen(EventReady); }), "ready event missing"); CheckStreamInfoEx(player); Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "enable video failed"); diff --git a/core/libs/tests/GuestCallTrace.cpp b/core/libs/tests/GuestCallTrace.cpp index 6fc77ae658..6721aaddac 100644 --- a/core/libs/tests/GuestCallTrace.cpp +++ b/core/libs/tests/GuestCallTrace.cpp @@ -12,8 +12,12 @@ #include #include #include +#include +#include +#include #include #include +#include #include #include @@ -199,4 +203,16 @@ int main(int argc, char** argv) { Require(owned == Window); Require(CopyCallCounts_nid_no_patch(counts, 1, &total) == 1 && total == 8 + Workers * Calls); Require(counts[0].calls == 1 + Workers * Calls && std::strcmp(counts[0].name, "Integers") == 0); + + const auto path = std::filesystem::temp_directory_path() / ("guest_call_trace_counts_" + std::to_string(CurrentThread()) + ".txt"); + const auto utf8 = path.u8string(); + Require(WriteCallCounts_nid_no_patch(reinterpret_cast(utf8.c_str()))); + std::ifstream written(path, std::ios::binary); + const std::string text{std::istreambuf_iterator(written), std::istreambuf_iterator()}; + written.close(); + Require(text == std::to_string(1 + Workers * Calls) + " libTraced.prx Integers\n3 libTraced.prx Blocks\n2 libTraced Doubles\n1 libOther.prx Mixed\n1 libOther Variadic\n"); + const auto unwritable = (path / "file").u8string(); + Require(!WriteCallCounts_nid_no_patch(reinterpret_cast(unwritable.c_str()))); + Require(!WriteCallCounts_nid_no_patch(nullptr) && !WriteCallCounts_nid_no_patch("")); + std::filesystem::remove(path); } diff --git a/core/libs/tests/GuestCes.cpp b/core/libs/tests/GuestCes.cpp index a9ffd3653d..bb5862cb5e 100644 --- a/core/libs/tests/GuestCes.cpp +++ b/core/libs/tests/GuestCes.cpp @@ -1,5 +1,6 @@ #include "prx/libSceCesCs/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -29,6 +30,320 @@ static int Utf8ToSbc(const std::uint8_t* profile, const char* utf8, std::uint32_ return sceCesUtf8ToSbc(reinterpret_cast(utf8), utf8max, &length, profile, &sbc); } +static void RequireSbcRoundTrip(const std::uint8_t* profile, std::initializer_list unassigned) { + constexpr int unassignedCode = static_cast(0x805C0020); + for (int sbc = 1; sbc < 256; ++sbc) { + std::uint8_t utf8[4] = {}; + std::uint32_t length = 7; + std::uint8_t back = 0; + if (std::find(unassigned.begin(), unassigned.end(), sbc) != unassigned.end()) { + Require(sceCesSbcToUtf8(profile, static_cast(sbc), utf8, sizeof(utf8), &length) == unassignedCode && length == 0); + continue; + } + Require(sceCesSbcToUtf8(profile, static_cast(sbc), utf8, sizeof(utf8), &length) == 0); + Require(sceCesUtf8ToSbc(utf8, length, &length, profile, &back) == 0 && back == sbc); + } +} + +struct MeasuredUtf8 { + const char* bytes; + std::uint32_t available; + int result; + std::uint32_t length; +}; + +static void RequireMeasuredUtf8ToSbc(const std::uint8_t* profile) { + constexpr MeasuredUtf8 cases[] = { + {"\xE2\x41", 1, static_cast(0x805C0011), 3}, + {"\xE2\x41", 2, static_cast(0x805C0014), 2}, + {"\xE2\x41\xAC", 3, static_cast(0x805C0014), 1}, + {"\xE2\x82\x41", 3, static_cast(0x805C0014), 2}, + {"\xE2\x82", 2, static_cast(0x805C0011), 3}, + {"\xE2\xC0", 2, static_cast(0x805C0014), 2}, + {"\xF0\x41\x98", 3, static_cast(0x805C0014), 2}, + {"\xF0\x9F\x41", 3, static_cast(0x805C0014), 3}, + {"\xF0\x41\x98\x80", 4, static_cast(0x805C0014), 1}, + {"\xF0\x9F\x98\x41", 4, static_cast(0x805C0014), 3}, + {"\xF0\x9F\x98", 3, static_cast(0x805C0011), 4}, + {"\xC3\x41", 2, static_cast(0x805C0014), 1}, + {"\xC3", 1, static_cast(0x805C0011), 2}, + {"\x80", 1, static_cast(0x805C0014), 0}, + {"\xFE", 1, static_cast(0x805C0014), 0}, + {"\xFF", 1, static_cast(0x805C0014), 0}, + {"\xC0\x80", 2, static_cast(0x805C0015), 2}, + {"\xC1\xBF", 2, static_cast(0x805C0015), 2}, + {"\xE0\x80\x80", 3, static_cast(0x805C0015), 3}, + {"\xE0\x9F\xBF", 3, static_cast(0x805C0015), 3}, + {"\xED\xA0\x80", 3, static_cast(0x805C0015), 3}, + {"\xF0\x8F\xBF\xBF", 4, static_cast(0x805C0015), 4}, + {"\xF8\x80\x80\x80\x80", 5, static_cast(0x805C0015), 5}, + {"\xFC\x80\x80\x80\x80\x80", 6, static_cast(0x805C0015), 6}, + {"\xE0\xA0\x80", 3, static_cast(0x805C0020), 3}, + {"\xED\x9F\xBF", 3, static_cast(0x805C0020), 3}, + {"\xEF\xBF\xBF", 3, static_cast(0x805C0020), 3}, + {"\xF0\x90\x80\x80", 4, static_cast(0x805C0020), 4}, + {"\xF4\x90\x80\x80", 4, static_cast(0x805C0020), 4}, + {"\xF5\x80\x80\x80", 4, static_cast(0x805C0020), 4}, + {"\xF7\xBF\xBF\xBF", 4, static_cast(0x805C0020), 4}, + {"\xF8\x88\x80\x80\x80", 4, static_cast(0x805C0011), 5}, + {"\xF8\x88\x80\x80\x80", 5, static_cast(0x805C0020), 5}, + {"\xFB\xBF\xBF\xBF\xBF", 5, static_cast(0x805C0020), 5}, + {"\xFC\x84\x80", 3, static_cast(0x805C0011), 6}, + {"\xFC\x84\x80\x80\x80\x80", 6, static_cast(0x805C0020), 6}, + {"\xFD\xBF\xBF\xBF\xBF\xBF", 6, static_cast(0x805C0020), 6}, + {"\xC2\x80", 2, static_cast(0x805C0020), 2}, + {"\xC2\x81", 2, static_cast(0x805C0020), 2}, + {"\xC2\x8D", 2, static_cast(0x805C0020), 2}, + {"\xC2\xA0", 2, 0, 2}, + {"\xC5\x92", 2, 0, 2}, + }; + for (const MeasuredUtf8& measured : cases) { + std::uint32_t length = 0xAAAAAAAA; + std::uint8_t sbc = 0xBB; + Require(sceCesUtf8ToSbc(reinterpret_cast(measured.bytes), measured.available, &length, profile, &sbc) == measured.result); + Require(length == measured.length); + Require(measured.result != 0 ? sbc == 0 : sbc != 0); + } +} + +constexpr std::uint32_t MeasuredEucJpPairs = 0x9295D4B1; +constexpr std::uint32_t MeasuredEucJpSupplementary = 0x75256134; + +struct MeasuredString { + const char* source; + std::uint32_t available; + std::uint32_t capacity; + int result; + std::uint32_t read; + std::uint32_t written; + const char* output; +}; + +static void RequireMeasuredStrings(void* latin, void* eucJp) { + constexpr int dstBufferEnd = static_cast(0x805C0031); + constexpr int invalidEncode = static_cast(0x805C0014); + constexpr int unassignedCode = static_cast(0x805C0020); + constexpr MeasuredString toUtf8[] = { + {"ABC", 3, 16, 0, 3, 3, "ABC"}, + {"ABC\0D", 5, 16, 0, 3, 3, "ABC"}, + {"ABC", 0, 16, 0, 3, 3, "ABC"}, + {"ABC", 3, 4, 0, 3, 3, "ABC"}, + {"ABC", 3, 3, dstBufferEnd, 2, 2, "AB"}, + {"ABC", 3, 2, dstBufferEnd, 1, 1, "A"}, + {"A\xE9" "B", 3, 16, 0, 3, 4, "A\xC3\xA9" "B"}, + {"A\xE9" "B", 3, 3, dstBufferEnd, 1, 1, "A"}, + {"\x80\x80\x80", 3, 7, dstBufferEnd, 2, 6, "\xE2\x82\xAC\xE2\x82\xAC"}, + {"A\x81" "B", 3, 16, unassignedCode, 1, 1, "A"}, + {"", 2, 16, 0, 0, 0, ""}, + }; + for (const MeasuredString& measured : toUtf8) { + std::uint8_t output[16]; + std::memset(output, 0xBB, sizeof(output)); + std::uint32_t read = 0xAAAAAAAA; + std::uint32_t written = 0xCCCCCCCC; + Require(sceCesMbcsStrToUtf8Str(latin, reinterpret_cast(measured.source), measured.available, &read, output, measured.capacity, &written) == measured.result); + Require(read == measured.read && written == measured.written); + Require(std::memcmp(output, measured.output, measured.written + 1) == 0 && output[measured.written + 1] == 0xBB); + Require(sceCesMbcsStrGetUtf8Len(latin, reinterpret_cast(measured.source), measured.available, &read, &written) == (measured.result == dstBufferEnd ? 0 : measured.result)); + } + constexpr MeasuredString fromUtf8[] = { + {"ABC", 3, 3, dstBufferEnd, 2, 2, "AB"}, + {"A\xC3\xA9" "B", 4, 16, 0, 4, 3, "A\xE9" "B"}, + {"A\xE4\xB8\x80" "B", 5, 16, unassignedCode, 1, 1, "A"}, + {"A\xC3", 2, 16, static_cast(0x805C0011), 1, 1, "A"}, + {"A\xFF" "B", 3, 16, invalidEncode, 1, 1, "A"}, + {"A\xC3\0B", 0, 16, invalidEncode, 1, 1, "A"}, + {"A\xC0\x80" "B", 4, 16, static_cast(0x805C0015), 1, 1, "A"}, + {"A\xED\xA0\x80" "B", 5, 16, static_cast(0x805C0015), 1, 1, "A"}, + {"A\xC2\x81" "B", 4, 16, unassignedCode, 1, 1, "A"}, + {"A\xF8\x88\x80\x80\x80" "B", 7, 16, unassignedCode, 1, 1, "A"}, + {"A\xE2\x41\xAC" "B", 5, 16, invalidEncode, 1, 1, "A"}, + {"ABC", 0, 16, 0, 3, 3, "ABC"}, + }; + for (const MeasuredString& measured : fromUtf8) { + std::uint8_t output[16]; + std::memset(output, 0xBB, sizeof(output)); + std::uint32_t read = 0xAAAAAAAA; + std::uint32_t written = 0xCCCCCCCC; + Require(sceCesUtf8StrToMbcsStr(latin, reinterpret_cast(measured.source), measured.available, &read, output, measured.capacity, &written) == measured.result); + Require(read == measured.read && written == measured.written); + Require(std::memcmp(output, measured.output, measured.written + 1) == 0 && output[measured.written + 1] == 0xBB); + } + constexpr MeasuredString eucToUtf8[] = { + {"A\xA4\xA2" "B", 0, 16, 0, 4, 5, "A\xE3\x81\x82" "B"}, + {"A\xA4\xA2" "B", 0, 4, dstBufferEnd, 1, 1, "A"}, + {"A\xA4\xA2" "B", 0, 5, dstBufferEnd, 3, 4, "A\xE3\x81\x82"}, + {"A\xA4", 2, 16, static_cast(0x805C0011), 1, 1, "A"}, + {"A\x0E" "B", 3, 16, invalidEncode, 1, 1, "A"}, + {"A\x80" "B", 0, 16, 0, 3, 4, "A\xC2\x80" "B"}, + {"A\xA4\0B", 0, 16, invalidEncode, 1, 1, "A"}, + }; + for (const MeasuredString& measured : eucToUtf8) { + std::uint8_t output[16]; + std::memset(output, 0xBB, sizeof(output)); + std::uint32_t read = 0xAAAAAAAA; + std::uint32_t written = 0xCCCCCCCC; + Require(sceCesMbcsStrToUtf8Str(eucJp, reinterpret_cast(measured.source), measured.available, &read, output, measured.capacity, &written) == measured.result); + Require(read == measured.read && written == measured.written); + Require(std::memcmp(output, measured.output, measured.written + 1) == 0 && output[measured.written + 1] == 0xBB); + } + + const auto* letters = reinterpret_cast("ABC"); + std::uint8_t output[4] = {0xBB, 0xBB, 0xBB, 0xBB}; + std::uint32_t read = 7; + std::uint32_t written = 7; + Require(sceCesMbcsStrToUtf8Str(latin, letters, 3, &read, output, 0, &written) == dstBufferEnd && read == 0 && written == 0 && output[0] == 0xBB); + Require(sceCesMbcsStrToUtf8Str(latin, letters, 3, &read, nullptr, 16, &written) == static_cast(0x805C0030) && read == 0 && written == 0); + Require(sceCesMbcsStrToUtf8Str(nullptr, letters, 3, &read, nullptr, 16, &written) == static_cast(0x805C0001)); + Require(sceCesMbcsStrToUtf8Str(latin, nullptr, 3, &read, nullptr, 16, &written) == static_cast(0x805C0010)); + Require(sceCesMbcsStrToUtf8Str(nullptr, letters, 3, &read, output, sizeof(output), &written) == static_cast(0x805C0001) && output[0] == 0 && output[1] == 0xBB); + output[0] = 0xBB; + Require(sceCesMbcsStrToUtf8Str(latin, nullptr, 3, &read, output, sizeof(output), &written) == static_cast(0x805C0010) && output[0] == 0 && read == 0 && written == 0); + output[0] = 0xBB; + Require(sceCesMbcsStrToUtf8Str(latin, nullptr, 3, &read, output, 0, &written) == static_cast(0x805C0010) && output[0] == 0xBB); + Require(sceCesMbcsStrGetUtf8Len(latin, reinterpret_cast("A\x81" "B"), 3, &read, &written) == unassignedCode && read == 1 && written == 1); + Require(sceCesMbcsStrGetUtf8Len(latin, nullptr, 3, &read, &written) == static_cast(0x805C0010) && read == 0 && written == 0); + Require(sceCesUtf8StrGetMbcsLen(latin, reinterpret_cast("A\xE4\xB8\x80" "B"), 5, &read, &written) == unassignedCode && read == 1 && written == 1); + Require(sceCesUtf8StrGetMbcsLen(latin, reinterpret_cast("A\xC3"), 2, &read, &written) == static_cast(0x805C0011) && read == 1 && written == 1); + + std::uint16_t wide[8]; + for (std::uint16_t& unit : wide) unit = 0xBBBB; + Require(sceCesMbcsStrToUcs2Str(latin, letters, 0, &read, wide, 3, &written) == dstBufferEnd && read == 2 && written == 2); + Require(wide[0] == u'A' && wide[1] == u'B' && wide[2] == 0 && wide[3] == 0xBBBB); + Require(sceCesMbcsStrToUcs2Str(latin, reinterpret_cast("A\x81" "B"), 3, &read, wide, 8, &written) == unassignedCode && read == 1 && written == 1 && wide[1] == 0); + const std::uint16_t han[] = {u'A', 0x4E00, u'B', 0}; + const std::uint16_t surrogate[] = {u'A', 0xD800, u'B', 0}; + const std::uint16_t control[] = {u'A', 0x0081, u'B', 0}; + const std::uint16_t plain[] = {u'A', u'B', u'C', 0}; + std::memset(output, 0xBB, sizeof(output)); + Require(sceCesUcs2StrToMbcsStr(latin, plain, 2, &read, output, sizeof(output), &written) == 0 && read == 2 && written == 2 && std::memcmp(output, "AB", 3) == 0); + Require(sceCesUcs2StrToMbcsStr(latin, plain, 0, &read, output, 3, &written) == dstBufferEnd && read == 2 && written == 2); + Require(sceCesUcs2StrToMbcsStr(latin, han, 0, &read, output, sizeof(output), &written) == unassignedCode && read == 1 && written == 1); + Require(sceCesUcs2StrToMbcsStr(latin, surrogate, 0, &read, output, sizeof(output), &written) == static_cast(0x805C0015) && read == 1 && written == 1); + Require(sceCesUcs2StrToMbcsStr(latin, control, 0, &read, output, sizeof(output), &written) == unassignedCode && read == 1 && written == 1); + Require(sceCesUcs2StrGetMbcsLen(latin, han, 0, &read, &written) == unassignedCode && read == 1 && written == 1); +} + +static std::uint32_t Fnv(std::uint32_t hash, std::uint32_t word) { + for (int k = 0; k < 4; ++k) { + hash ^= (word >> (8 * k)) & 0xFFu; + hash *= 16777619u; + } + return hash; +} + +constexpr std::uint32_t MeasuredEucJpEncoder = 0x3AF3CD39; +constexpr std::uint32_t MeasuredSjisEncoder = 0x2FB8906B; + +static std::uint32_t EncoderSweep(void* context) { + std::uint32_t hash = 2166136261u; + for (std::uint32_t value = 0; value <= 0xFFFF; ++value) { + const std::uint16_t source[1] = {static_cast(value)}; + std::uint8_t output[4] = {0xBB, 0xBB, 0xBB, 0xBB}; + std::uint32_t used = 0xAAAAAAAA; + std::uint32_t produced = 0xCCCCCCCC; + const int result = sceCesUcs2StrToMbcsStr(context, source, 1, &used, output, sizeof(output), &produced); + std::uint32_t bytes = 0; + std::memcpy(&bytes, output, sizeof(bytes)); + hash = Fnv(Fnv(Fnv(Fnv(hash, static_cast(result)), produced), bytes), used); + } + return hash; +} + +constexpr std::uint32_t MeasuredSjisDecoder = 0xCA99178C; + +static std::uint32_t SjisDecoderSweep(void* sjis) { + std::uint32_t hash = 2166136261u; + for (std::uint32_t lead = 0; lead < 256; ++lead) { + for (std::uint32_t trail = 0; trail < 256; ++trail) { + const std::uint8_t source[2] = {static_cast(lead), static_cast(trail)}; + std::uint16_t output[3] = {0xBBBB, 0xBBBB, 0xBBBB}; + std::uint32_t used = 0xAAAAAAAA; + std::uint32_t produced = 0xCCCCCCCC; + const int result = sceCesMbcsStrToUcs2Str(sjis, source, 2, &used, output, 3, &produced); + hash = Fnv(Fnv(Fnv(Fnv(Fnv(hash, static_cast(result)), used), produced), static_cast(output[0]) | static_cast(output[1]) << 16), output[2]); + } + } + return hash; +} + +static void RequireMeasuredEncoders(void* eucJp, void* sjis) { + Require(SjisDecoderSweep(sjis) == MeasuredSjisDecoder); + Require(EncoderSweep(eucJp) == MeasuredEucJpEncoder); + Require(EncoderSweep(sjis) == MeasuredSjisEncoder); + const std::uint16_t aliases[] = {0x00A2, 0x00A3, 0x00AC, 0x2014, 0x2016, 0x203E, 0x2212, 0x301C, 0xFEFF, 0x0080, 0}; + std::uint8_t output[32] = {}; + std::uint32_t used = 0; + std::uint32_t produced = 0; + Require(sceCesUcs2StrToMbcsStr(eucJp, aliases, 0, &used, output, sizeof(output), &produced) == 0 && used == 10 && produced == 17); + Require(std::memcmp(output, "\xA1\xF1\xA1\xF2\xA2\xCC\xA1\xBD\xA1\xC2\xA1\xB1\xA1\xDD\xA1\xC1\x80", 18) == 0); + Require(sceCesUcs2StrToMbcsStr(sjis, aliases, 0, &used, output, sizeof(output), &produced) == static_cast(0x805C0024) && used == 9 && produced == 16); + Require(std::memcmp(output, "\x81\x91\x81\x92\x81\xCA\x81\x5C\x81\x61\x81\x50\x81\x7C\x81\x60", 17) == 0); + const std::uint16_t shift[] = {u'A', 0x000E, 0}; + Require(sceCesUcs2StrToMbcsStr(sjis, shift, 0, &used, output, sizeof(output), &produced) == 0 && produced == 2); + Require(sceCesUcs2StrToMbcsStr(eucJp, shift, 0, &used, output, sizeof(output), &produced) == static_cast(0x805C0020) && used == 1 && produced == 1); +} + +static void RequireMeasuredEucJp(void* eucJp) { + constexpr int invalidProfile = static_cast(0x805C0004); + constexpr int invalidSrcBuffer = static_cast(0x805C0010); + constexpr int srcBufferEnd = static_cast(0x805C0011); + constexpr int invalidEncode = static_cast(0x805C0014); + constexpr int unassignedCode = static_cast(0x805C0020); + constexpr int invalidDstBuffer = static_cast(0x805C0030); + constexpr int dstBufferEnd = static_cast(0x805C0031); + std::uint32_t pairs = 2166136261u; + for (int lead = 0; lead < 256; ++lead) { + for (int trail = 0; trail < 256; ++trail) { + const std::uint8_t cell[2] = {static_cast(lead), static_cast(trail)}; + std::uint32_t used = 0xAAAAAAAA; + std::uint16_t code = 0xBBBB; + const int result = sceCesEucJpToUcs2(eucJp, cell, 2, &used, &code); + pairs = Fnv(Fnv(Fnv(Fnv(pairs, static_cast(cell[1])), static_cast(result)), used), code); + } + } + Require(pairs == MeasuredEucJpPairs); + std::uint32_t supplementary = 2166136261u; + for (int second = 0; second < 256; ++second) { + for (int third = 0; third < 256; ++third) { + const std::uint8_t cell[3] = {0x8F, static_cast(second), static_cast(third)}; + std::uint32_t used = 0xAAAAAAAA; + std::uint16_t code = 0xBBBB; + const int result = sceCesEucJpToUcs2(eucJp, cell, 3, &used, &code); + supplementary = Fnv(Fnv(Fnv(Fnv(supplementary, static_cast(third)), static_cast(result)), used), code); + } + } + Require(supplementary == MeasuredEucJpSupplementary); + + const auto* hiragana = reinterpret_cast("\xA4\xA2"); + std::uint32_t used = 7; + std::uint32_t produced = 7; + std::uint16_t code = 0xBBBB; + std::uint8_t utf8[4] = {0xBB, 0xBB, 0xBB, 0xBB}; + Require(sceCesEucJpToUcs2(nullptr, hiragana, 2, &used, &code) == invalidProfile && used == 0 && code == 0); + Require(sceCesEucJpToUcs2(eucJp, nullptr, 2, &used, &code) == invalidSrcBuffer && used == 0); + Require(sceCesEucJpToUcs2(eucJp, hiragana, 0, &used, nullptr) == invalidSrcBuffer && used == 0); + Require(sceCesEucJpToUcs2(eucJp, hiragana, 1, &used, nullptr) == srcBufferEnd && used == 2); + Require(sceCesEucJpToUcs2(eucJp, hiragana, 2, &used, nullptr) == invalidDstBuffer && used == 2); + Require(sceCesEucJpToUcs2(eucJp, hiragana, 2, nullptr, &code) == 0 && code == 0x3042); + Require(sceCesEucJpToUtf8(eucJp, hiragana, 2, &used, utf8, sizeof(utf8), &produced) == 0 && used == 2 && produced == 3 && std::memcmp(utf8, "\xE3\x81\x82", 3) == 0); + Require(sceCesEucJpToUtf8(eucJp, reinterpret_cast("\x8E\xB1"), 2, &used, utf8, sizeof(utf8), &produced) == 0 && used == 2 && produced == 3 && std::memcmp(utf8, "\xEF\xBD\xB1", 3) == 0); + Require(sceCesEucJpToUtf8(eucJp, reinterpret_cast("\x80"), 1, &used, utf8, sizeof(utf8), &produced) == 0 && used == 1 && produced == 2 && std::memcmp(utf8, "\xC2\x80", 2) == 0); + std::memset(utf8, 0xBB, sizeof(utf8)); + Require(sceCesEucJpToUtf8(nullptr, nullptr, 2, &used, utf8, sizeof(utf8), &produced) == invalidProfile && used == 0 && produced == 0); + Require(sceCesEucJpToUtf8(eucJp, nullptr, 0, &used, utf8, sizeof(utf8), &produced) == invalidSrcBuffer && used == 0 && produced == 0); + Require(sceCesEucJpToUtf8(eucJp, hiragana, 1, &used, nullptr, sizeof(utf8), &produced) == srcBufferEnd && used == 2 && produced == 0); + Require(sceCesEucJpToUtf8(eucJp, reinterpret_cast("\xA4\x20"), 2, &used, utf8, sizeof(utf8), &produced) == invalidEncode && used == 2 && produced == 0); + Require(sceCesEucJpToUtf8(eucJp, reinterpret_cast("\x8E\xE0"), 2, &used, utf8, sizeof(utf8), &produced) == unassignedCode && used == 2 && produced == 0); + Require(sceCesEucJpToUtf8(eucJp, reinterpret_cast("\xA9\xA1"), 2, &used, utf8, sizeof(utf8), &produced) == unassignedCode && used == 2 && produced == 0); + Require(sceCesEucJpToUtf8(eucJp, reinterpret_cast("\xFF\xA1"), 2, &used, utf8, sizeof(utf8), &produced) == invalidEncode && used == 1 && produced == 0); + Require(sceCesEucJpToUtf8(eucJp, hiragana, 2, &used, nullptr, sizeof(utf8), &produced) == invalidDstBuffer && used == 2 && produced == 3); + Require(sceCesEucJpToUtf8(eucJp, hiragana, 2, &used, utf8, 2, &produced) == dstBufferEnd && used == 2 && produced == 3); + Require(sceCesEucJpToUtf8(eucJp, hiragana, 2, &used, utf8, 0, &produced) == dstBufferEnd && used == 2 && produced == 3); + Require(utf8[0] == 0xBB); + Require(sceCesEucJpToUtf8(eucJp, hiragana, 2, nullptr, utf8, sizeof(utf8), nullptr) == 0 && std::memcmp(utf8, "\xE3\x81\x82", 3) == 0); +} + static void RequireSbcCode(const std::uint8_t* profile, std::uint8_t sbc, std::uint32_t code) { char expected[4] = {}; if (code < 0x80) { @@ -46,7 +361,7 @@ static void RequireSbcCode(const std::uint8_t* profile, std::uint8_t sbc, std::u static void CheckSingleByteProfiles() { constexpr int unassignedCode = static_cast(0x805C0020); - const auto getters = {sceCesRefersUcsProfileIso8859_1, sceCesRefersUcsProfileIso8859_2, sceCesRefersUcsProfileIso8859_4, sceCesRefersUcsProfileIso8859_5, sceCesRefersUcsProfileIso8859_9, sceCesRefersUcsProfileIso8859_10, sceCesRefersUcsProfileIso8859_13, sceCesRefersUcsProfileIso8859_14, sceCesRefersUcsProfileIso8859_15, sceCesRefersUcsProfileIso8859_16, sceCesRefersUcsProfileCp437, sceCesRefersUcsProfileCp737, sceCesRefersUcsProfileCp775, sceCesRefersUcsProfileCp850, sceCesRefersUcsProfileCp852, sceCesRefersUcsProfileCp855, sceCesRefersUcsProfileCp860, sceCesRefersUcsProfileCp861, sceCesRefersUcsProfileCp862, sceCesRefersUcsProfileCp863, sceCesRefersUcsProfileCp865, sceCesRefersUcsProfileCp866, sceCesRefersUcsProfileKoi8R, sceCesRefersUcsProfileKoi8U, sceCesRefersUcsProfileCp1254, sceCesRefersUcsProfileCp1256, sceCesRefersUcsProfileCp1258}; + const auto getters = {sceCesRefersUcsProfileIso8859_1, sceCesRefersUcsProfileIso8859_2, sceCesRefersUcsProfileIso8859_4, sceCesRefersUcsProfileIso8859_5, sceCesRefersUcsProfileIso8859_9, sceCesRefersUcsProfileIso8859_10, sceCesRefersUcsProfileIso8859_13, sceCesRefersUcsProfileIso8859_14, sceCesRefersUcsProfileIso8859_15, sceCesRefersUcsProfileIso8859_16, sceCesRefersUcsProfileCp437, sceCesRefersUcsProfileCp737, sceCesRefersUcsProfileCp775, sceCesRefersUcsProfileCp850, sceCesRefersUcsProfileCp852, sceCesRefersUcsProfileCp855, sceCesRefersUcsProfileCp860, sceCesRefersUcsProfileCp861, sceCesRefersUcsProfileCp862, sceCesRefersUcsProfileCp863, sceCesRefersUcsProfileCp865, sceCesRefersUcsProfileCp866, sceCesRefersUcsProfileKoi8R, sceCesRefersUcsProfileKoi8U, sceCesRefersUcsProfileCp1254, sceCesRefersUcsProfileCp1256, sceCesRefersUcsProfileCp1258, sceCesRefersUcsProfileCp858}; const void* existing[] = {sceCesRefersUcsProfileCp1252(), sceCesRefersUcsProfileCp1251(), sceCesRefersUcsProfileCp1250()}; std::size_t index = 0; for (const auto getter : getters) { @@ -183,6 +498,10 @@ static void CheckSingleByteProfiles() { {sceCesRefersUcsProfileCp1258, 0xA4, 0x00A4}, {sceCesRefersUcsProfileCp1258, 0xC1, 0x00C1}, {sceCesRefersUcsProfileCp1258, 0xFF, 0x00FF}, + {sceCesRefersUcsProfileCp850, 0xD5, 0x0131}, + {sceCesRefersUcsProfileCp858, 0x80, 0x00C7}, + {sceCesRefersUcsProfileCp858, 0xD5, 0x20AC}, + {sceCesRefersUcsProfileCp858, 0xFF, 0x00A0}, }; for (const Check& check : checks) RequireSbcCode(check.profile(), check.sbc, check.code); @@ -214,7 +533,6 @@ int main() { RequireSbcToUtf8(profile, 'A', "A"); RequireSbcToUtf8(profile, 0x80, "\xE2\x82\xAC"); RequireSbcToUtf8(profile, 0x9F, "\xC5\xB8"); - RequireSbcToUtf8(profile, 0x81, "\xC2\x81"); RequireSbcToUtf8(profile, 0xE9, "\xC3\xA9"); RequireSbcToUtf8(profile, 0xFF, "\xC3\xBF"); @@ -222,29 +540,48 @@ int main() { RequireUtf8ToSbc(profile, "\xE2\x82\xAC", 0x80); RequireUtf8ToSbc(profile, "\xE2\x84\xA2", 0x99); RequireUtf8ToSbc(profile, "\xC3\xA9", 0xE9); - RequireUtf8ToSbc(profile, "\xC2\x8D", 0x8D); - for (int sbc = 1; sbc < 256; ++sbc) { - std::uint8_t utf8[4] = {}; - std::uint32_t length = 0; - std::uint8_t back = 0; - Require(sceCesSbcToUtf8(profile, static_cast(sbc), utf8, sizeof(utf8), &length) == 0); - Require(sceCesUtf8ToSbc(utf8, length, &length, profile, &back) == 0 && back == sbc); - } + RequireSbcRoundTrip(profile, {0x81, 0x8D, 0x8F, 0x90, 0x9D}); + RequireMeasuredUtf8ToSbc(profile); std::uint8_t utf8[4] = {}; std::uint32_t length = 0; std::uint8_t sbc = 0; - Require(sceCesSbcToUtf8(nullptr, 'A', utf8, sizeof(utf8), &length) == invalidParameter); - Require(sceCesSbcToUtf8(profile, 'A', nullptr, sizeof(utf8), &length) == invalidDstBuffer); - Require(sceCesSbcToUtf8(profile, 0x80, utf8, 2, &length) == dstBufferEnd); - Require(sceCesUtf8ToSbc(utf8, 1, &length, nullptr, &sbc) == invalidParameter); - Require(sceCesUtf8ToSbc(nullptr, 1, &length, profile, &sbc) == invalidSrcBuffer); - Require(sceCesUtf8ToSbc(utf8, 1, &length, profile, nullptr) == invalidDstBuffer); - Require(Utf8ToSbc(profile, "A", 0) == srcBufferEnd); + constexpr int invalidProfile = static_cast(0x805C0004); + constexpr int illegalCode = static_cast(0x805C0015); + length = 7; + Require(sceCesSbcToUtf8(nullptr, 'A', utf8, sizeof(utf8), &length) == invalidProfile && length == 0); + Require(sceCesSbcToUtf8(nullptr, 0x81, utf8, sizeof(utf8), &length) == invalidProfile && length == 0); + Require(sceCesSbcToUtf8(profile, 0x80, nullptr, sizeof(utf8), &length) == invalidDstBuffer && length == 3); + Require(sceCesSbcToUtf8(profile, 0x81, nullptr, sizeof(utf8), &length) == unassignedCode && length == 0); + Require(sceCesSbcToUtf8(profile, 0x81, utf8, 0, &length) == unassignedCode && length == 0); + utf8[0] = 0xBB; + Require(sceCesSbcToUtf8(profile, 0x80, utf8, 2, &length) == dstBufferEnd && length == 3 && utf8[0] == 0xBB); + Require(sceCesSbcToUtf8(profile, 'A', utf8, 0, &length) == dstBufferEnd && length == 1 && utf8[0] == 0xBB); + Require(sceCesSbcToUtf8(profile, 0xE9, utf8, 1, nullptr) == dstBufferEnd); + const auto* euro = reinterpret_cast("\xE2\x82\xAC"); + const auto* han = reinterpret_cast("\xE4\xB8\x80"); + const auto* letter = reinterpret_cast("A"); + sbc = 0xBB; + Require(sceCesUtf8ToSbc(letter, 1, &length, nullptr, &sbc) == invalidProfile && length == 1 && sbc == 0); + Require(sceCesUtf8ToSbc(letter, 1, &length, nullptr, nullptr) == invalidProfile && length == 1); + Require(sceCesUtf8ToSbc(euro, 3, &length, nullptr, &sbc) == invalidProfile && length == 3); + Require(sceCesUtf8ToSbc(euro, 2, &length, nullptr, &sbc) == srcBufferEnd && length == 3); + Require(sceCesUtf8ToSbc(reinterpret_cast("\xFF"), 1, &length, nullptr, &sbc) == invalidEncode && length == 0); + sbc = 0xBB; + Require(sceCesUtf8ToSbc(nullptr, 1, &length, nullptr, &sbc) == invalidSrcBuffer && length == 0 && sbc == 0); + Require(sceCesUtf8ToSbc(nullptr, 1, &length, profile, nullptr) == invalidSrcBuffer && length == 0); + sbc = 0xBB; + Require(sceCesUtf8ToSbc(letter, 0, &length, profile, &sbc) == invalidSrcBuffer && length == 0 && sbc == 0); + Require(sceCesUtf8ToSbc(euro, 3, &length, profile, nullptr) == invalidDstBuffer && length == 3); + Require(sceCesUtf8ToSbc(han, 3, &length, profile, nullptr) == unassignedCode && length == 3); + sbc = 0xBB; + Require(sceCesUtf8ToSbc(han, 3, &length, profile, &sbc) == unassignedCode && length == 3 && sbc == 0); + Require(sceCesUtf8ToSbc(euro, 3, nullptr, profile, &sbc) == 0 && sbc == 0x80); + Require(Utf8ToSbc(profile, "A", 0) == invalidSrcBuffer); Require(Utf8ToSbc(profile, "\xE2\x82\xAC", 2) == srcBufferEnd); Require(Utf8ToSbc(profile, "\x80", 1) == invalidEncode); - Require(Utf8ToSbc(profile, "\xC0\xAF", 2) == invalidEncode); - Require(Utf8ToSbc(profile, "\xED\xA0\x80", 3) == invalidEncode); + Require(Utf8ToSbc(profile, "\xC0\xAF", 2) == illegalCode); + Require(Utf8ToSbc(profile, "\xED\xA0\x80", 3) == illegalCode); Require(Utf8ToSbc(profile, "\xE3\x81\x82", 3) == unassignedCode); Require(Utf8ToSbc(profile, "\xF0\x9F\x98\x80", 4) == unassignedCode); @@ -258,38 +595,25 @@ int main() { RequireSbcToUtf8(cyrillic, 0xB9, "\xE2\x84\x96"); RequireUtf8ToSbc(cyrillic, "\xD0\x9F", 0xCF); RequireUtf8ToSbc(cyrillic, "\xD1\x91", 0xB8); - for (int sbc = 1; sbc < 256; ++sbc) { - std::uint8_t cp[4] = {}; - std::uint32_t cpLength = 0; - std::uint8_t back = 0; - Require(sceCesSbcToUtf8(cyrillic, static_cast(sbc), cp, sizeof(cp), &cpLength) == 0); - Require(sceCesUtf8ToSbc(cp, cpLength, &cpLength, cyrillic, &back) == 0 && back == sbc); - } + RequireSbcRoundTrip(cyrillic, {0x98}); Require(Utf8ToSbc(cyrillic, "\xC3\xA9", 2) == unassignedCode); Require(Utf8ToSbc(profile, "\xD0\x90", 2) == unassignedCode); - const void* latin2 = sceCesRefersUcsProfileCp1250(); + const auto* latin2 = static_cast(sceCesRefersUcsProfileCp1250()); Require(latin2 != nullptr && latin2 == sceCesRefersUcsProfileCp1250() && latin2 != profile && latin2 != cyrillic); - RequireSbcToUtf8(reinterpret_cast(latin2), 'A', "A"); - RequireSbcToUtf8(reinterpret_cast(latin2), 0x80, "\xE2\x82\xAC"); - RequireSbcToUtf8(reinterpret_cast(latin2), 0x81, "\xC2\x81"); - RequireSbcToUtf8(reinterpret_cast(latin2), 0x8A, "\xC5\xA0"); - RequireSbcToUtf8(reinterpret_cast(latin2), 0xA3, "\xC5\x81"); - RequireSbcToUtf8(reinterpret_cast(latin2), 0xA5, "\xC4\x84"); - RequireSbcToUtf8(reinterpret_cast(latin2), 0xAF, "\xC5\xBB"); - RequireSbcToUtf8(reinterpret_cast(latin2), 0xE9, "\xC3\xA9"); - RequireSbcToUtf8(reinterpret_cast(latin2), 0xFF, "\xCB\x99"); - RequireUtf8ToSbc(reinterpret_cast(latin2), "\xC5\xA0", 0x8A); - RequireUtf8ToSbc(reinterpret_cast(latin2), "\xC5\x81", 0xA3); - RequireUtf8ToSbc(reinterpret_cast(latin2), "\xC3\xA9", 0xE9); - for (int sbc = 1; sbc < 256; ++sbc) { - std::uint8_t cp[4] = {}; - std::uint32_t cpLength = 0; - std::uint8_t back = 0; - Require(sceCesSbcToUtf8(reinterpret_cast(latin2), static_cast(sbc), cp, sizeof(cp), &cpLength) == 0); - Require(sceCesUtf8ToSbc(cp, cpLength, &cpLength, reinterpret_cast(latin2), &back) == 0 && back == sbc); - } - Require(Utf8ToSbc(reinterpret_cast(latin2), "\xD0\x90", 2) == unassignedCode); + RequireSbcToUtf8(latin2, 'A', "A"); + RequireSbcToUtf8(latin2, 0x80, "\xE2\x82\xAC"); + RequireSbcToUtf8(latin2, 0x8A, "\xC5\xA0"); + RequireSbcToUtf8(latin2, 0xA3, "\xC5\x81"); + RequireSbcToUtf8(latin2, 0xA5, "\xC4\x84"); + RequireSbcToUtf8(latin2, 0xAF, "\xC5\xBB"); + RequireSbcToUtf8(latin2, 0xE9, "\xC3\xA9"); + RequireSbcToUtf8(latin2, 0xFF, "\xCB\x99"); + RequireUtf8ToSbc(latin2, "\xC5\xA0", 0x8A); + RequireUtf8ToSbc(latin2, "\xC5\x81", 0xA3); + RequireUtf8ToSbc(latin2, "\xC3\xA9", 0xE9); + RequireSbcRoundTrip(latin2, {0x81, 0x83, 0x88, 0x90, 0x98}); + Require(Utf8ToSbc(latin2, "\xD0\x90", 2) == unassignedCode); char latin2Context[256] = {}; Require(sceCesMbcsUcsContextInit(latin2Context, latin2) == 0); std::uint8_t latin2Back[16] = {}; @@ -309,7 +633,7 @@ int main() { char sjisContext[256] = {}; Require(sceCesMbcsUcsContextInit(sjisContext, sjisSheet) == 0); const auto* eucSource = reinterpret_cast("Go \xEF\xBD\xB1!"); - std::uint8_t mbcs[16] = {}; + std::uint8_t mbcs[32] = {}; std::uint32_t used = 0; std::uint32_t produced = 0; char sheet[1024] = {}; @@ -322,8 +646,12 @@ int main() { Require(used == 7 && produced == 6 && std::memcmp(mbcs, "Go \x8E\xB1!", 6) == 0); Require(sceCesUtf8StrGetMbcsLen(eucContext, eucSource, 16, &used, &produced) == 0 && used == 7 && produced == 6); Require(sceCesUtf8StrToMbcsStr(eucContext, eucSource, 16, &used, mbcs, 4, &produced) == dstBufferEnd); + Require(used == 3 && produced == 3 && std::memcmp(mbcs, "Go ", 4) == 0); + Require(sceCesUtf8StrToMbcsStr(eucContext, eucSource, 16, &used, mbcs, sizeof(mbcs), &produced) == 0); Require(sceCesUtf8StrToMbcsStr(nullptr, eucSource, 16, &used, mbcs, sizeof(mbcs), &produced) == invalidParameter); Require(sceCesUtf8StrToMbcsStr(eucContext, reinterpret_cast("\xF0\x9F\x98\x80"), 4, &used, mbcs, sizeof(mbcs), &produced) == unassignedCode); + Require(used == 0 && produced == 0 && mbcs[0] == 0); + Require(sceCesUtf8StrToMbcsStr(eucContext, eucSource, 16, &used, mbcs, sizeof(mbcs), &produced) == 0); std::uint8_t back[16] = {}; Require(sceCesMbcsStrToUtf8Str(nullptr, mbcs, 6, &used, back, sizeof(back), &produced) == invalidParameter); @@ -338,8 +666,8 @@ int main() { Require(sceCesUcs2StrToMbcsStr(eucContext, ucs2, 4, &used, mbcs, sizeof(mbcs), &produced) == 0); Require(used == 3 && produced == 4 && std::memcmp(mbcs, "OK\x8E\xDF", 4) == 0); Require(sceCesUcs2StrGetMbcsLen(eucContext, ucs2, 4, &used, &produced) == 0 && produced == 4); - std::uint16_t wide[8] = {}; - Require(sceCesMbcsStrToUcs2Str(eucContext, mbcs, 4, &used, wide, 8, &produced) == 0); + std::uint16_t wide[16] = {}; + Require(sceCesMbcsStrToUcs2Str(eucContext, mbcs, 4, &used, wide, 16, &produced) == 0); Require(used == 4 && produced == 3 && wide[0] == u'O' && wide[1] == u'K' && wide[2] == 0xFF9F); Require(sceCesMbcsStrGetUcs2Len(eucContext, mbcs, 4, &used, &produced) == 0 && produced == 3); std::uint16_t single = 0; @@ -351,15 +679,15 @@ int main() { Require(used == 24 && produced == 16 && std::memcmp(mbcs, "\xA4\xB3\xA4\xF3\xA4\xCB\xA4\xC1\xA4\xCF\xC6\xFC\xCB\xDC\xB8\xEC", 16) == 0); std::uint8_t japaneseBack[32] = {}; Require(sceCesMbcsStrToUtf8Str(eucContext, mbcs, 16, &used, japaneseBack, sizeof(japaneseBack), &produced) == 0 && produced == 24 && std::memcmp(japaneseBack, japanese, 24) == 0); - Require(sceCesMbcsStrToUcs2Str(eucContext, mbcs, 16, &used, wide, 8, &produced) == 0 && used == 16 && produced == 8 && wide[0] == 0x3053 && wide[5] == 0x65E5 && wide[7] == 0x8A9E); - Require(sceCesMbcsStrToUcs2Str(eucContext, mbcs, 15, &used, wide, 8, &produced) == srcBufferEnd); + Require(sceCesMbcsStrToUcs2Str(eucContext, mbcs, 16, &used, wide, 16, &produced) == 0 && used == 16 && produced == 8 && wide[0] == 0x3053 && wide[5] == 0x65E5 && wide[7] == 0x8A9E); + Require(sceCesMbcsStrToUcs2Str(eucContext, mbcs, 15, &used, wide, 16, &produced) == srcBufferEnd); const std::uint16_t extensions[] = {0x2460, 0xFF5E, 0x2252, 0}; Require(sceCesUcs2StrToMbcsStr(eucContext, extensions, 4, &used, mbcs, sizeof(mbcs), &produced) == 0 && produced == 6 && std::memcmp(mbcs, "\xAD\xA1\xA1\xC1\xA2\xE2", 6) == 0); - Require(sceCesMbcsStrToUcs2Str(eucContext, reinterpret_cast("\xA9\xA1"), 2, &used, wide, 8, &produced) == unassignedCode); - Require(sceCesMbcsStrToUcs2Str(eucContext, reinterpret_cast("\xA4\x41"), 2, &used, wide, 8, &produced) == invalidEncode); - Require(sceCesMbcsStrToUcs2Str(eucContext, reinterpret_cast("\xA4"), 1, &used, wide, 8, &produced) == srcBufferEnd); + Require(sceCesMbcsStrToUcs2Str(eucContext, reinterpret_cast("\xA9\xA1"), 2, &used, wide, 16, &produced) == unassignedCode); + Require(sceCesMbcsStrToUcs2Str(eucContext, reinterpret_cast("\xA4\x41"), 2, &used, wide, 16, &produced) == invalidEncode); + Require(sceCesMbcsStrToUcs2Str(eucContext, reinterpret_cast("\xA4"), 1, &used, wide, 16, &produced) == srcBufferEnd); const std::uint16_t yen[] = {0x00A5, 0}; - Require(sceCesUcs2StrToMbcsStr(eucContext, yen, 2, &used, mbcs, sizeof(mbcs), &produced) == unassignedCode); + Require(sceCesUcs2StrToMbcsStr(eucContext, yen, 2, &used, mbcs, sizeof(mbcs), &produced) == 0 && used == 1 && produced == 2 && std::memcmp(mbcs, "\xA1\xEF", 3) == 0); std::uint32_t assigned = 0; for (std::uint32_t lead = 0xA1; lead <= 0xFE; ++lead) { for (std::uint32_t trail = 0xA1; trail <= 0xFE; ++trail) { @@ -378,47 +706,36 @@ int main() { Require(assigned == 7336); std::uint16_t supplementary = 0; - Require(sceCesEucJpToUcs2(eucJp, reinterpret_cast("\x8F\xA2\xAF"), 3, &used, &supplementary) == 0 && used == 3 && supplementary == 0x02D8); - Require(sceCesEucJpToUcs2(eucJp, reinterpret_cast("\x8F\xA2"), 2, &used, &supplementary) == srcBufferEnd); + Require(sceCesEucJpToUcs2(eucJp, reinterpret_cast("\x8F\xA2\xAF"), 3, &used, &supplementary) == invalidEncode && used == 3 && supplementary == 0); + Require(sceCesEucJpToUcs2(eucJp, reinterpret_cast("\x8F\xA2"), 2, &used, &supplementary) == srcBufferEnd && used == 3); Require(sceCesEucJpToUcs2(eucJp, reinterpret_cast("\x8F\x41\xA1"), 3, &used, &supplementary) == invalidEncode); Require(sceCesEucJpToUcs2(eucJp, reinterpret_cast("\x8F\xA2\x41"), 3, &used, &supplementary) == invalidEncode); Require(sceCesEucJpToUcs2(eucJp, reinterpret_cast("\x8F\xA1\xA1"), 3, &used, &supplementary) == unassignedCode); Require(sceCesEucJpToUcs2(eucJp, reinterpret_cast("\x8F\xF5\xA1"), 3, &used, &supplementary) == unassignedCode); - const std::uint16_t supplementaryText[] = {0x02D8, 0x0386, 0x0402, 0x6600, 0}; - Require(sceCesUcs2StrToMbcsStr(eucContext, supplementaryText, 5, &used, mbcs, sizeof(mbcs), &produced) == 0); - Require(used == 4 && produced == 11 && std::memcmp(mbcs, "\x8F\xA2\xAF\x8F\xA6\xE1\x8F\xA7\xC2\xFA\xB4", 11) == 0); - Require(sceCesUcs2StrGetMbcsLen(eucContext, supplementaryText, 5, &used, &produced) == 0 && produced == 11); - std::uint16_t supplementaryBack[8] = {}; - Require(sceCesMbcsStrToUcs2Str(eucContext, mbcs, 11, &used, supplementaryBack, 8, &produced) == 0); - Require(used == 11 && produced == 4 && supplementaryBack[0] == 0x02D8 && supplementaryBack[1] == 0x0386 && supplementaryBack[2] == 0x0402 && supplementaryBack[3] == 0x6600); - Require(sceCesMbcsStrGetUcs2Len(eucContext, mbcs, 11, &used, &produced) == 0 && produced == 4); - Require(sceCesMbcsStrToUtf8Str(eucContext, mbcs, 11, &used, japaneseBack, sizeof(japaneseBack), &produced) == 0); - Require(produced == 9 && std::memcmp(japaneseBack, "\xCB\x98\xCE\x86\xD0\x82\xE6\x98\x80", 9) == 0); - Require(sceCesMbcsStrGetUtf8Len(eucContext, mbcs, 11, &used, &produced) == 0 && produced == 9); - Require(sceCesMbcsStrToUcs2Str(eucContext, mbcs, 10, &used, supplementaryBack, 8, &produced) == srcBufferEnd); - std::uint32_t supplementaryAssigned = 0; - for (std::uint32_t lead = 0xA2; lead <= 0xF4; ++lead) { - for (std::uint32_t trail = 0xA1; trail <= 0xFE; ++trail) { - const std::uint8_t cell[3] = {0x8F, static_cast(lead), static_cast(trail)}; - std::uint16_t decoded[2] = {}; - if (sceCesEucJpToUcs2(eucJp, cell, 3, &used, decoded) != 0) continue; - ++supplementaryAssigned; - decoded[1] = 0; - std::uint8_t again[4] = {}; - std::uint16_t second[2] = {}; - Require(sceCesUcs2StrToMbcsStr(eucContext, decoded, 2, &used, again, sizeof(again), &produced) == 0); - if (produced == 3) Require(again[1] == cell[1] && again[2] == cell[2]); - Require(sceCesEucJpToUcs2(eucJp, again, produced, &used, second) == 0 && second[0] == decoded[0]); - } - } - Require(supplementaryAssigned == 6067); + const std::uint16_t supplementaryText[] = {u'A', 0x02D8, 0}; + Require(sceCesUcs2StrToMbcsStr(eucContext, supplementaryText, 0, &used, mbcs, sizeof(mbcs), &produced) == unassignedCode); + Require(used == 1 && produced == 1 && std::memcmp(mbcs, "A", 2) == 0); + const std::uint16_t ibmText[] = {0x6600, 0x2170, 0}; + Require(sceCesUcs2StrToMbcsStr(eucContext, ibmText, 0, &used, mbcs, sizeof(mbcs), &produced) == 0); + Require(used == 2 && produced == 4 && std::memcmp(mbcs, "\xFA\xB4\xFC\xF1", 5) == 0); + Require(sceCesMbcsStrToUtf8Str(eucContext, reinterpret_cast("A\x8F\xA2\xAF" "B"), 0, &used, japaneseBack, sizeof(japaneseBack), &produced) == invalidEncode); + Require(used == 1 && produced == 1 && std::memcmp(japaneseBack, "A", 2) == 0); + RequireMeasuredEucJp(eucJp); + char measuredLatin[256] = {}; + Require(sceCesMbcsUcsContextInit(measuredLatin, profile) == 0); + RequireMeasuredStrings(measuredLatin, eucContext); + Require(sceCesMbcsUcsContextInit(measuredLatin, nullptr) == invalidParameter); const auto* sjisSource = reinterpret_cast("Go \xEF\xBD\xB1!"); Require(sceCesUtf8StrToMbcsStr(sjisContext, sjisSource, 16, &used, mbcs, sizeof(mbcs), &produced) == 0); Require(used == 7 && produced == 5 && std::memcmp(mbcs, "Go \xB1!", 5) == 0); Require(sceCesUtf8StrGetMbcsLen(sjisContext, sjisSource, 16, &used, &produced) == 0 && used == 7 && produced == 5); Require(sceCesUtf8StrToMbcsStr(sjisContext, sjisSource, 16, &used, mbcs, 4, &produced) == dstBufferEnd); + Require(used == 3 && produced == 3 && std::memcmp(mbcs, "Go ", 4) == 0); + Require(sceCesUtf8StrToMbcsStr(sjisContext, sjisSource, 16, &used, mbcs, sizeof(mbcs), &produced) == 0); Require(sceCesUtf8StrToMbcsStr(sjisContext, reinterpret_cast("\xF0\x9F\x98\x80"), 4, &used, mbcs, sizeof(mbcs), &produced) == unassignedCode); + Require(used == 0 && produced == 0 && mbcs[0] == 0); + Require(sceCesUtf8StrToMbcsStr(sjisContext, sjisSource, 16, &used, mbcs, sizeof(mbcs), &produced) == 0); std::uint8_t sjisBack[16] = {}; Require(sceCesMbcsStrToUtf8Str(sjisContext, mbcs, 5, &used, sjisBack, sizeof(sjisBack), &produced) == 0); @@ -432,8 +749,8 @@ int main() { Require(sceCesUcs2StrToMbcsStr(sjisContext, sjisUcs2, 4, &used, mbcs, sizeof(mbcs), &produced) == 0); Require(used == 3 && produced == 3 && std::memcmp(mbcs, "OK\xDF", 3) == 0); Require(sceCesUcs2StrGetMbcsLen(sjisContext, sjisUcs2, 4, &used, &produced) == 0 && produced == 3); - std::uint16_t sjisWide[8] = {}; - Require(sceCesMbcsStrToUcs2Str(sjisContext, mbcs, 3, &used, sjisWide, 8, &produced) == 0); + std::uint16_t sjisWide[16] = {}; + Require(sceCesMbcsStrToUcs2Str(sjisContext, mbcs, 3, &used, sjisWide, 16, &produced) == 0); Require(used == 3 && produced == 3 && sjisWide[0] == u'O' && sjisWide[1] == u'K' && sjisWide[2] == 0xFF9F); Require(sceCesMbcsStrGetUcs2Len(sjisContext, mbcs, 3, &used, &produced) == 0 && produced == 3); @@ -441,19 +758,20 @@ int main() { Require(used == 24 && produced == 16 && std::memcmp(mbcs, "\x82\xB1\x82\xF1\x82\xC9\x82\xBF\x82\xCD\x93\xFA\x96\x7B\x8C\xEA", 16) == 0); std::uint8_t sjisJapaneseBack[32] = {}; Require(sceCesMbcsStrToUtf8Str(sjisContext, mbcs, 16, &used, sjisJapaneseBack, sizeof(sjisJapaneseBack), &produced) == 0 && produced == 24 && std::memcmp(sjisJapaneseBack, japanese, 24) == 0); - Require(sceCesMbcsStrToUcs2Str(sjisContext, mbcs, 16, &used, sjisWide, 8, &produced) == 0 && used == 16 && produced == 8 && sjisWide[0] == 0x3053 && sjisWide[5] == 0x65E5 && sjisWide[7] == 0x8A9E); - Require(sceCesMbcsStrToUcs2Str(sjisContext, mbcs, 15, &used, sjisWide, 8, &produced) == srcBufferEnd); + Require(sceCesMbcsStrToUcs2Str(sjisContext, mbcs, 16, &used, sjisWide, 16, &produced) == 0 && used == 16 && produced == 8 && sjisWide[0] == 0x3053 && sjisWide[5] == 0x65E5 && sjisWide[7] == 0x8A9E); + Require(sceCesMbcsStrToUcs2Str(sjisContext, mbcs, 15, &used, sjisWide, 16, &produced) == srcBufferEnd); const std::uint16_t sjisExtensions[] = {0x2460, 0xFF5E, 0x2252, 0}; Require(sceCesUcs2StrToMbcsStr(sjisContext, sjisExtensions, 4, &used, mbcs, sizeof(mbcs), &produced) == 0 && produced == 6 && std::memcmp(mbcs, "\x87\x40\x81\x60\x81\xE0", 6) == 0); - Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\x85\x40"), 2, &used, sjisWide, 8, &produced) == unassignedCode); - Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\x80"), 1, &used, sjisWide, 8, &produced) == invalidEncode); - Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xA0"), 1, &used, sjisWide, 8, &produced) == invalidEncode); - Require(sceCesUcs2StrToMbcsStr(sjisContext, yen, 2, &used, mbcs, sizeof(mbcs), &produced) == unassignedCode); - Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xFB\xFC"), 2, &used, sjisWide, 8, &produced) == 0 && used == 2 && produced == 1 && sjisWide[0] == 0x9AD9); - Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xFA\x40"), 2, &used, sjisWide, 8, &produced) == 0 && used == 2 && produced == 1 && sjisWide[0] == 0x2170); - Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xFC\x4B"), 2, &used, sjisWide, 8, &produced) == 0 && used == 2 && produced == 1 && sjisWide[0] == 0x9ED1); - Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xFC\x4C"), 2, &used, sjisWide, 8, &produced) == unassignedCode); - Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xF0\x40"), 2, &used, sjisWide, 8, &produced) == unassignedCode); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\x85\x40"), 2, &used, sjisWide, 16, &produced) == unassignedCode); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\x80"), 1, &used, sjisWide, 16, &produced) == invalidEncode); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xA0"), 1, &used, sjisWide, 16, &produced) == invalidEncode); + Require(sceCesUcs2StrToMbcsStr(sjisContext, yen, 2, &used, mbcs, sizeof(mbcs), &produced) == 0 && used == 1 && produced == 2 && std::memcmp(mbcs, "\x81\x8F", 3) == 0); + RequireMeasuredEncoders(eucContext, sjisContext); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xFB\xFC"), 2, &used, sjisWide, 16, &produced) == 0 && used == 2 && produced == 1 && sjisWide[0] == 0x9AD9); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xFA\x40"), 2, &used, sjisWide, 16, &produced) == 0 && used == 2 && produced == 1 && sjisWide[0] == 0x2170); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xFC\x4B"), 2, &used, sjisWide, 16, &produced) == 0 && used == 2 && produced == 1 && sjisWide[0] == 0x9ED1); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xFC\x4C"), 2, &used, sjisWide, 16, &produced) == unassignedCode); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xF0\x40"), 2, &used, sjisWide, 16, &produced) == unassignedCode); std::uint32_t sjisAssigned = 0; for (std::uint32_t lead = 0x81; lead <= 0xFC; ++lead) { if (lead > 0x9F && lead < 0xE0) continue; diff --git a/core/libs/tests/GuestCesChinese.cpp b/core/libs/tests/GuestCesChinese.cpp index 409b46af74..424a2a27bd 100644 --- a/core/libs/tests/GuestCesChinese.cpp +++ b/core/libs/tests/GuestCesChinese.cpp @@ -12,6 +12,7 @@ static constexpr int invalidParameter = static_cast(0x805C0001); static constexpr int invalidSrcBuffer = static_cast(0x805C0010); static constexpr int srcBufferEnd = static_cast(0x805C0011); static constexpr int invalidEncode = static_cast(0x805C0014); + constexpr int outOfCodeSpace = static_cast(0x805C0015); static constexpr int unassignedCode = static_cast(0x805C0020); static constexpr int invalidDstBuffer = static_cast(0x805C0030); static constexpr int dstBufferEnd = static_cast(0x805C0031); @@ -81,7 +82,7 @@ static std::uint32_t Gb18030Decode(const void* gb, std::uint32_t packed) { std::uint32_t code = 0xDEADBEEF; const int result = sceCesGbToUtf32(gb, bytes, size, &consumed, &code); if (result != 0) { - Require(result == unassignedCode && consumed == 0x55 && code == 0xDEADBEEF); + Require(result == unassignedCode && consumed == size && code == 0); return 0; } Require(consumed == size); @@ -97,12 +98,13 @@ static std::uint32_t Gb18030Encode(const void* gb, std::uint32_t code) { return packed; } -static void RequireGb18030Error(const void* gb, const char* bytes, std::uint32_t size, int error) { +static void RequireGb18030Error(const void* gb, const char* bytes, std::uint32_t size, int error, std::uint32_t length) { std::uint32_t consumed = 0x55; std::uint32_t utf32 = 0xDEADBEEF; - Require(sceCesGbToUtf32(gb, Bytes(bytes), size, &consumed, &utf32) == error && consumed == 0x55 && utf32 == 0xDEADBEEF); + Require(sceCesGbToUtf32(gb, Bytes(bytes), size, &consumed, &utf32) == error && consumed == length && utf32 == 0); std::uint16_t ucs2 = 0xBEEF; - Require(sceCesMbcToUcs2(gb, Bytes(bytes), size, &consumed, &ucs2) == error && consumed == 0x55 && ucs2 == 0xBEEF); + consumed = 0x55; + Require(sceCesMbcToUcs2(gb, Bytes(bytes), size, &consumed, &ucs2) == error && consumed == length && ucs2 == 0); } static void CheckGb18030(const void* gb, bool revision2005, std::uint32_t twoByteHash, std::uint32_t fourByteHash, std::uint32_t encodeHash, int fourByteOneWay, int codeOneWay) { @@ -163,25 +165,25 @@ static void CheckGb18030(const void* gb, bool revision2005, std::uint32_t twoByt if (revision2005 && entry.fallback) Require(Gb18030Encode(gb, entry.code2000) == entry.bytes); } - RequireGb18030Error(gb, "\x80", 1, invalidEncode); - RequireGb18030Error(gb, "\xFF", 1, invalidEncode); - RequireGb18030Error(gb, "\x81", 1, srcBufferEnd); - RequireGb18030Error(gb, "\x81\x30", 2, srcBufferEnd); - RequireGb18030Error(gb, "\x81\x30\x81", 3, srcBufferEnd); - RequireGb18030Error(gb, "\x81\x00", 2, invalidEncode); - RequireGb18030Error(gb, "\x81\x2F", 2, invalidEncode); - RequireGb18030Error(gb, "\x81\x3A", 2, invalidEncode); - RequireGb18030Error(gb, "\x81\x7F", 2, invalidEncode); - RequireGb18030Error(gb, "\x81\xFF", 2, invalidEncode); - RequireGb18030Error(gb, "\x81\x30\x80\x30", 4, invalidEncode); - RequireGb18030Error(gb, "\x81\x30\xFF\x30", 4, invalidEncode); - RequireGb18030Error(gb, "\x81\x30\x30\x30", 4, invalidEncode); - RequireGb18030Error(gb, "\x81\x30\x81\x2F", 4, invalidEncode); - RequireGb18030Error(gb, "\x81\x30\x81\x3A", 4, invalidEncode); - RequireGb18030Error(gb, "\x81\x30\x81\x81", 4, invalidEncode); - RequireGb18030Error(gb, "\x84\x31\xA5\x30", 4, unassignedCode); - RequireGb18030Error(gb, "\xE3\x32\x9A\x36", 4, unassignedCode); - RequireGb18030Error(gb, "\xFE\x39\xFE\x39", 4, unassignedCode); + RequireGb18030Error(gb, "\x80", 1, invalidEncode, 1); + RequireGb18030Error(gb, "\xFF", 1, invalidEncode, 1); + RequireGb18030Error(gb, "\x81", 1, srcBufferEnd, 2); + RequireGb18030Error(gb, "\x81\x30", 2, srcBufferEnd, 4); + RequireGb18030Error(gb, "\x81\x30\x81", 3, srcBufferEnd, 4); + RequireGb18030Error(gb, "\x81\x00", 2, invalidEncode, 1); + RequireGb18030Error(gb, "\x81\x2F", 2, invalidEncode, 2); + RequireGb18030Error(gb, "\x81\x3A", 2, invalidEncode, 2); + RequireGb18030Error(gb, "\x81\x7F", 2, invalidEncode, 2); + RequireGb18030Error(gb, "\x81\xFF", 2, invalidEncode, 2); + RequireGb18030Error(gb, "\x81\x30\x80\x30", 4, invalidEncode, 4); + RequireGb18030Error(gb, "\x81\x30\xFF\x30", 4, invalidEncode, 4); + RequireGb18030Error(gb, "\x81\x30\x30\x30", 4, invalidEncode, 4); + RequireGb18030Error(gb, "\x81\x30\x81\x2F", 4, invalidEncode, 4); + RequireGb18030Error(gb, "\x81\x30\x81\x3A", 4, invalidEncode, 4); + RequireGb18030Error(gb, "\x81\x30\x81\x81", 4, invalidEncode, 4); + RequireGb18030Error(gb, "\x84\x31\xA5\x30", 4, unassignedCode, 4); + RequireGb18030Error(gb, "\xE3\x32\x9A\x36", 4, unassignedCode, 4); + RequireGb18030Error(gb, "\xFE\x39\xFE\x39", 4, unassignedCode, 4); std::uint32_t consumed = 0x55; std::uint32_t produced = 0x55; @@ -202,8 +204,8 @@ static void CheckGb18030(const void* gb, bool revision2005, std::uint32_t twoByt std::uint8_t mbcs[4] = {1, 2, 3, 4}; produced = 0x55; - Require(sceCesUtf32ToGb(0x10000, gb, mbcs, 3, &produced) == dstBufferEnd && produced == 0x55 && std::memcmp(mbcs, "\x01\x02\x03\x04", 4) == 0); - Require(sceCesUtf32ToGb(0xD800, gb, mbcs, sizeof(mbcs), &produced) == invalidEncode); + Require(sceCesUtf32ToGb(0x10000, gb, mbcs, 3, &produced) == dstBufferEnd && produced == 4 && std::memcmp(mbcs, "\x01\x02\x03\x04", 4) == 0); + Require(sceCesUtf32ToGb(0xD800, gb, mbcs, sizeof(mbcs), &produced) == outOfCodeSpace); Require(sceCesUtf32ToGb(0x110000, gb, mbcs, sizeof(mbcs), &produced) == invalidEncode); Require(sceCesUcs2ToGb(0x80, gb, mbcs, sizeof(mbcs), &produced) == 0 && produced == 4 && std::memcmp(mbcs, "\x81\x30\x81\x30", 4) == 0); Require(sceCesUcs2ToGb(0x20AC, gb, mbcs, sizeof(mbcs), &produced) == 0 && produced == 2 && std::memcmp(mbcs, "\xA2\xE3", 2) == 0); @@ -227,7 +229,8 @@ static void CheckGb18030(const void* gb, bool revision2005, std::uint32_t twoByt std::uint16_t backUtf16[8] = {}; Require(sceCesMbcsStrToUtf16Str(context, text, gbSize, &consumed, backUtf16, 8, &produced) == 0 && consumed == gbSize && produced == 6); Require(backUtf16[0] == 'A' && backUtf16[1] == 0x4E2D && backUtf16[2] == 0x80 && backUtf16[3] == 0xD83D && backUtf16[4] == 0xDE00 && backUtf16[5] == 0xFE10); - Require(sceCesMbcsStrToUtf16Str(context, text, gbSize, &consumed, backUtf16, 4, &produced) == dstBufferEnd); + std::uint16_t shortUtf16[4] = {}; + Require(sceCesMbcsStrToUtf16Str(context, text, gbSize, &consumed, shortUtf16, 4, &produced) == dstBufferEnd); Require(sceCesMbcsStrGetUtf16Len(context, text, gbSize, &consumed, &produced) == 0 && consumed == gbSize && produced == 6); std::uint8_t again[16] = {}; Require(sceCesUtf16StrToMbcsStr(context, backUtf16, 6, &consumed, again, sizeof(again), &produced) == 0 && consumed == 6 && produced == gbSize); @@ -289,7 +292,7 @@ int main() { Require(sceCesUcs2ToEucCn(0x4E02, euc, mbcs, sizeof(mbcs), &produced) == unassignedCode); Require(sceCesUcs2ToEucCn(0x20AC, euc, mbcs, sizeof(mbcs), &produced) == unassignedCode); Require(sceCesUcs2ToEucCn(0x4E2D, euc, mbcs, 1, &produced) == dstBufferEnd); - Require(sceCesUcs2ToEucCn(0xDC00, euc, mbcs, sizeof(mbcs), &produced) == invalidEncode); + Require(sceCesUcs2ToEucCn(0xDC00, euc, mbcs, sizeof(mbcs), &produced) == outOfCodeSpace); Require(sceCesUcs2ToGb(0x4E02, gbk, mbcs, sizeof(mbcs), &produced) == 0 && produced == 2 && std::memcmp(mbcs, "\x81\x40", 2) == 0); Require(sceCesUcs2ToGb(0x20AC, gbk, mbcs, sizeof(mbcs), &produced) == 0 && produced == 1 && mbcs[0] == 0x80); Require(sceCesUcs2ToGb(0x3042, euc, mbcs, sizeof(mbcs), &produced) == invalidParameter); diff --git a/core/libs/tests/GuestCesDbcs.cpp b/core/libs/tests/GuestCesDbcs.cpp index 044d48e641..bd3edaa068 100644 --- a/core/libs/tests/GuestCesDbcs.cpp +++ b/core/libs/tests/GuestCesDbcs.cpp @@ -40,6 +40,7 @@ constexpr int invalidParameter = static_cast(0x805C0001); constexpr int invalidSrcBuffer = static_cast(0x805C0010); constexpr int srcBufferEnd = static_cast(0x805C0011); constexpr int invalidEncode = static_cast(0x805C0014); +constexpr int illegalCode = static_cast(0x805C0015); constexpr int unassignedCode = static_cast(0x805C0020); constexpr int invalidDstBuffer = static_cast(0x805C0030); constexpr int dstBufferEnd = static_cast(0x805C0031); @@ -85,7 +86,8 @@ static int Scalar(ToUtf8 convert, const void* profile, const char* source, std:: std::uint32_t consumed = 77; std::uint32_t produced = 77; const int result = convert(profile, Bytes(source), sourceMax, &consumed, utf8, utf8Max, &produced); - if (result != 0) Require(consumed == 77 && produced == 77 && std::memcmp(utf8, "\x55\x55\x55\x55", 4) == 0); + const bool decodeError = source && sourceMax > 0 && (result == static_cast(0x805C0011) || result == static_cast(0x805C0014) || result == static_cast(0x805C0020)); + if (result != 0) Require((decodeError ? consumed != 77 && produced == 0 : consumed == 77 && produced == 77) && std::memcmp(utf8, "\x55\x55\x55\x55", 4) == 0); return result; } @@ -112,15 +114,15 @@ static std::uint32_t RoundTripMismatches(void* context) { for (std::uint32_t lead = 0x81; lead < 0xFF; ++lead) { for (std::uint32_t trail = 0x40; trail < 0xFF; ++trail) { const std::uint8_t source[2] = {static_cast(lead), static_cast(trail)}; - std::uint16_t ucs2 = 0; + std::uint16_t ucs2[2] = {}; std::uint32_t read = 0; std::uint32_t written = 0; - if (sceCesMbcsStrToUcs2Str(context, source, sizeof(source), &read, &ucs2, 1, &written) != 0) continue; - Require(read == 2 && written == 1); - std::uint8_t back[2] = {}; - Require(sceCesUcs2StrToMbcsStr(context, &ucs2, 1, &read, back, sizeof(back), &written) == 0); - Require(read == 1 && written == 2); - if (std::memcmp(source, back, sizeof(back)) != 0) ++mismatches; + if (sceCesMbcsStrToUcs2Str(context, source, sizeof(source), &read, ucs2, 2, &written) != 0) continue; + Require(read == 2 && written == 1 && ucs2[1] == 0); + std::uint8_t back[3] = {}; + Require(sceCesUcs2StrToMbcsStr(context, ucs2, 1, &read, back, sizeof(back), &written) == 0); + Require(read == 1 && written == 2 && back[2] == 0); + if (std::memcmp(source, back, sizeof(source)) != 0) ++mismatches; } } return mismatches; @@ -183,17 +185,17 @@ static void RequireDecoders(const Decoders& decoders, const void* profile) { std::uint16_t ucs2 = 0x5555; std::uint32_t read = 77; Require(decoders.ucs2(profile, source, sizeof(source), &read, &ucs2) == expected); - Require(expected == 0 ? read == consumed && ucs2 == code : read == 77 && ucs2 == 0x5555); + Require(read == consumed && ucs2 == (expected == 0 ? code : 0)); for (int order = 0; order < 3; ++order) { std::uint16_t utf16 = 0x5555; std::uint32_t units = 77; read = 77; Require(decoders.utf16[order](profile, source, sizeof(source), &read, &utf16, 1, &units) == expected); - Require(expected == 0 ? read == consumed && units == 1 && Load(&utf16, 2, order) == code : read == 77 && units == 77 && utf16 == 0x5555); + Require(read == consumed && (expected == 0 ? units == 1 && Load(&utf16, 2, order) == code : units == 0 && utf16 == 0x5555)); std::uint32_t utf32 = 0x55555555; read = 77; Require(decoders.utf32[order](profile, source, sizeof(source), &read, &utf32) == expected); - Require(expected == 0 ? read == consumed && Load(&utf32, 4, order) == code : read == 77 && utf32 == 0x55555555); + Require(read == consumed && (expected == 0 ? Load(&utf32, 4, order) == code : utf32 == 0)); } } } @@ -237,7 +239,7 @@ static void RequireEncode(const Encoders& encoders, const void* profile, std::ui Encoded out; Require(Encode(encoders, form, code, profile, out) == expected); if (expected == 0) Require(out.read == SourceUnits(form, code) && out.size == size && std::memcmp(out.bytes, bytes, size) == 0); - else Require(out.read == 77 && out.size == 77 && out.bytes[0] == 0x55 && out.bytes[1] == 0x55); + else Require(out.read == 77 && out.size == ((form == 0 || form >= 5) && expected != static_cast(0x805C0001) ? 0u : 77u) && out.bytes[0] == 0x55 && out.bytes[1] == 0x55); } } @@ -246,7 +248,7 @@ static void RequireEncoders(const Encoders& encoders, const void* profile, void* for (std::uint32_t code = 1; code < 0x10000; ++code) { if (code >= 0xD800 && code <= 0xDFFF) continue; const auto ucs2 = static_cast(code); - std::uint8_t bytes[2] = {}; + std::uint8_t bytes[3] = {}; std::uint32_t read = 0; std::uint32_t written = 0; const int expected = sceCesUcs2StrToMbcsStr(context, &ucs2, 1, &read, bytes, sizeof(bytes), &written); @@ -271,8 +273,10 @@ int main() { void* eucJp = sceCesUcsProfileInitEucJpCp51932(eucJpSheet); Require(eucKr == eucKrSheet && uhc == uhcSheet && big5 == big5Sheet && eucJp == eucJpSheet); - RequireTable(sceCesEucKrToUtf8, eucKr, 0x01FF720E36F4AD67ull, 8226); - RequireTable(sceCesUhcToUtf8, uhc, 0x3B861A7E911985ABull, 17048); + RequireTable(sceCesEucKrToUtf8, eucKr, 0x18F4D7FD5B554273ull, 8227); + RequireTable(sceCesUhcToUtf8, uhc, 0x33B12AA5FAB04C43ull, 17049); + RequireScalar(sceCesEucKrToUtf8, eucKr, "\xA2\xE8", "\xE3\x89\xBE"); + RequireScalar(sceCesUhcToUtf8, uhc, "\xA2\xE8", "\xE3\x89\xBE"); RequireTable(sceCesBig5ToUtf8, big5, 0xE4D0D6A8019F35D9ull, 13503); RequireScalar(sceCesEucKrToUtf8, eucKr, "\xB0\xA1", "\xEA\xB0\x80"); @@ -318,8 +322,8 @@ int main() { Require(sceCesMbcsUcsContextInit(context, big5) == 0); RequireString(context, "\xA4\xA4\xA4\xE5", "\xE4\xB8\xAD\xE6\x96\x87"); - RequireString(context, "\xF9\xF9\xA2\x7E\xA4\x51\xA4\xCA", "\xE2\x95\x90\xE2\x95\xAD\xE5\x8D\x81\xE5\x8D\x85"); - RequireString(context, "\xF9\xE9\xF9\xEB\xF9\xEA\xA2\xA1\xA2\xA3\xA2\xA2", "\xE2\x95\x9E\xE2\x95\xA1\xE2\x95\xAA\xE2\x95\xAE\xE2\x95\xAF\xE2\x95\xB0"); + RequireString(context, "\xA2\xA4\xA2\x7E\xA4\x51\xA4\xCA", "\xE2\x95\x90\xE2\x95\xAD\xE5\x8D\x81\xE5\x8D\x85"); + RequireString(context, "\xA2\xA5\xA2\xA7\xA2\xA6\xA2\xA1\xA2\xA3\xA2\xA2", "\xE2\x95\x9E\xE2\x95\xA1\xE2\x95\xAA\xE2\x95\xAE\xE2\x95\xAF\xE2\x95\xB0"); Require(Utf8ToMbcs(context, "\xEA\xB0\x80") == unassignedCode); Require(RoundTripMismatches(context) == 10); @@ -362,7 +366,10 @@ int main() { RequireEncode(eucKrEncoders, eucKr, 0x20AC, 0, Bytes("\xA2\xE6"), 2); RequireEncode(eucKrEncoders, eucKr, 0xB620, unassignedCode, nullptr, 0); RequireEncode(uhcEncoders, uhc, 0xB620, 0, Bytes("\x8C\x63"), 2); - RequireEncode(big5Encoders, big5, 0x2550, 0, Bytes("\xF9\xF9"), 2); + RequireEncode(big5Encoders, big5, 0x2550, 0, Bytes("\xA2\xA4"), 2); + RequireEncode(big5Encoders, big5, 0x255E, 0, Bytes("\xA2\xA5"), 2); + RequireEncode(big5Encoders, big5, 0x2561, 0, Bytes("\xA2\xA7"), 2); + RequireEncode(big5Encoders, big5, 0x256A, 0, Bytes("\xA2\xA6"), 2); RequireEncode(big5Encoders, big5, 0x5341, 0, Bytes("\xA4\x51"), 2); RequireEncode(big5Encoders, big5, 0xAC00, unassignedCode, nullptr, 0); RequireEncode(eucKrEncoders, uhc, 0xAC00, invalidParameter, nullptr, 0); @@ -375,9 +382,9 @@ int main() { const std::uint32_t big = 0x0000AC00; Require(sceCesUcs2ToEucKr(0xAC00, uhc, nullptr, 0, nullptr) == invalidParameter); Require(sceCesUcs2ToEucKr(0xD800, eucKr, nullptr, 0, nullptr) == invalidDstBuffer); - Require(sceCesUcs2ToEucKr(0xD800, eucKr, out, 2, &size) == invalidEncode); + Require(sceCesUcs2ToEucKr(0xD800, eucKr, out, 2, &size) == illegalCode); Require(sceCesUtf32ToUhc(0x110000, uhc, out, 2, &size) == invalidEncode); - Require(sceCesUtf32ToUhc(0xDFFF, uhc, out, 2, &size) == invalidEncode); + Require(sceCesUtf32ToUhc(0xDFFF, uhc, out, 2, &size) == illegalCode); Require(sceCesUtf32beToBig5(nullptr, eucKr, nullptr, 0, nullptr) == invalidParameter); Require(sceCesUtf32beToBig5(nullptr, big5, nullptr, 0, nullptr) == invalidSrcBuffer); Require(sceCesUtf32leToBig5(&big, big5, nullptr, 0, nullptr) == invalidDstBuffer); @@ -386,14 +393,14 @@ int main() { Require(sceCesUtf8ToEucKr(Bytes("\xEA\xB0\x80"), 0, nullptr, eucKr, nullptr, 0, nullptr) == srcBufferEnd); Require(sceCesUtf8ToEucKr(Bytes("\xEA\xB0\x80"), 3, nullptr, eucKr, nullptr, 0, nullptr) == invalidDstBuffer); Require(sceCesUtf8ToEucKr(Bytes("\xEA\xB0\x80"), 2, &read, eucKr, out, 2, &size) == srcBufferEnd); - Require(sceCesUtf8ToEucKr(Bytes("\xC0\x80"), 2, &read, eucKr, out, 2, &size) == invalidEncode); + Require(sceCesUtf8ToEucKr(Bytes("\xC0\x80"), 2, &read, eucKr, out, 2, &size) == illegalCode); Require(sceCesUtf8ToEucKr(Bytes("\xEA\xB0\x80"), 3, &read, eucKr, out, 1, &size) == dstBufferEnd); const std::uint16_t lowSurrogate[1] = {0xDC00}; const std::uint16_t highSurrogate[2] = {0xD800, 0x0041}; Require(sceCesUtf16ToUhc(lowSurrogate, 1, &read, uhc, out, 2, &size) == invalidEncode); Require(sceCesUtf16ToUhc(highSurrogate, 1, &read, uhc, out, 2, &size) == srcBufferEnd); Require(sceCesUtf16ToUhc(highSurrogate, 2, &read, uhc, out, 2, &size) == invalidEncode); - Require(out[0] == 0x55 && out[1] == 0x55 && size == 77 && read == 77); + Require(out[0] == 0x55 && out[1] == 0x55 && size == 0 && read == 77); std::memcpy(&unit, "\xAC\x00", 2); Require(sceCesUtf16beToEucKr(&unit, 1, &read, eucKr, out, 2, &size) == 0 && read == 1 && size == 2 && std::memcmp(out, "\xB0\xA1", 2) == 0); diff --git a/core/libs/tests/GuestCesJapanese.cpp b/core/libs/tests/GuestCesJapanese.cpp index a63a45411d..42b8e2c16e 100644 --- a/core/libs/tests/GuestCesJapanese.cpp +++ b/core/libs/tests/GuestCesJapanese.cpp @@ -1,8 +1,10 @@ #include "prx/libSceCesCs/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include +#include #include #include @@ -13,24 +15,14 @@ static const std::uint8_t* Bytes(const char* text) { return reinterpret_cast(0x805C0001); constexpr int srcBufferEnd = static_cast(0x805C0011); constexpr int invalidEncode = static_cast(0x805C0014); +constexpr int outOfCodeSpace = static_cast(0x805C0015); constexpr int unassignedCode = static_cast(0x805C0020); constexpr int dstBufferEnd = static_cast(0x805C0031); -constexpr std::uint32_t jisX0208Cells = 6878; -constexpr std::uint32_t jisX0208Digest = 0xA614D6F6; +constexpr std::uint32_t jisX0208Cells = 6879; +constexpr std::uint32_t eucJisX0208Digest = 0xC1C98702; +constexpr std::uint32_t sjisJisX0208Digest = 0x0CE46B27; constexpr std::uint32_t jisX0212Cells = 6067; constexpr std::uint32_t jisX0212Digest = 0x7C193A73; -const std::string unsettledCell = "libSceCesCs: the console's mapping of JIS X 0208 cell 0x2140 (U+005C or U+FF3C) is unknown"; -const std::string unsettledByte = "libSceCesCs: the console's mapping of Shift-JIS bytes 0x5C and 0x7E (ASCII or JIS X 0201 Roman) is unknown"; - -template -static bool Reports(const std::string& message, TCall call) { - try { - call(); - } catch (const std::runtime_error& error) { - return error.what() == message; - } - return false; -} static std::uint32_t Digest(std::uint32_t hash, std::uint16_t value) { hash = (hash ^ (value & 0xFF)) * 0x01000193u; @@ -63,14 +55,15 @@ static bool EncodeLeavesOutputs(int expected, const void* profile, std::uint16_t std::uint8_t mbcs[4] = {0xEE, 0xEE, 0xEE, 0xEE}; std::uint32_t size = 77; const int result = sjis ? sceCesUcs2ToSJis(ucs2, profile, mbcs, sizeof(mbcs), &size) : sceCesUcs2ToEucJp(ucs2, profile, mbcs, sizeof(mbcs), &size); - return result == expected && size == 77 && mbcs[0] == 0xEE && mbcs[1] == 0xEE && mbcs[2] == 0xEE; + return result == expected && size == (expected == static_cast(0x805C0001) ? 77u : 0u) && mbcs[0] == 0xEE && mbcs[1] == 0xEE && mbcs[2] == 0xEE; } -static bool DecodeLeavesOutputs(int expected, const void* profile, const char* bytes, std::uint32_t size, bool sjis) { +static bool DecodeRejects(int expected, std::uint32_t length, const void* profile, const char* bytes, std::uint32_t size, bool sjis) { std::uint32_t used = 77; std::uint16_t ucs2 = 0xEEEE; const int result = sjis ? sceCesSJisToUcs2(profile, Bytes(bytes), size, &used, &ucs2) : sceCesEucJpToUcs2(profile, Bytes(bytes), size, &used, &ucs2); - return result == expected && used == 77 && ucs2 == 0xEEEE; + if (!sjis) return result == expected && ucs2 == 0; + return result == expected && used == length && ucs2 == 0; } static void SweepEucJisX0208(const void* profile) { @@ -82,9 +75,7 @@ static void SweepEucJisX0208(const void* profile) { std::uint32_t used = 77; std::uint16_t ucs2 = 0xEEEE; std::uint16_t value = 0; - if (lead == 0xA1 && trail == 0xC0) { - Require(Reports(unsettledCell, [&] { sceCesEucJpToUcs2(profile, cell, 2, &used, &ucs2); })); - } else if (const int result = sceCesEucJpToUcs2(profile, cell, 2, &used, &ucs2); result == 0) { + if (const int result = sceCesEucJpToUcs2(profile, cell, 2, &used, &ucs2); result == 0) { Require(used == 2); value = ucs2; ++assigned; @@ -92,12 +83,12 @@ static void SweepEucJisX0208(const void* profile) { std::uint32_t size = 0; Require(sceCesUcs2ToEucJp(ucs2, profile, again, sizeof(again), &size) == 0 && size == 2 && again[0] == lead && again[1] == trail); } else { - Require(result == unassignedCode && used == 77 && ucs2 == 0xEEEE); + Require(result == unassignedCode && used == 2 && ucs2 == 0); } hash = Digest(hash, value); } } - Require(assigned == jisX0208Cells && hash == jisX0208Digest); + Require(assigned == jisX0208Cells && hash == eucJisX0208Digest); } static void SweepSJisJisX0208(const void* profile) { @@ -111,9 +102,7 @@ static void SweepSJisJisX0208(const void* profile) { std::uint32_t used = 77; std::uint16_t ucs2 = 0xEEEE; std::uint16_t value = 0; - if (row == 1 && column == 32) { - Require(Reports(unsettledCell, [&] { sceCesSJisToUcs2(profile, cell, 2, &used, &ucs2); })); - } else if (const int result = sceCesSJisToUcs2(profile, cell, 2, &used, &ucs2); result == 0) { + if (const int result = sceCesSJisToUcs2(profile, cell, 2, &used, &ucs2); result == 0) { Require(used == 2); value = ucs2; ++assigned; @@ -121,12 +110,12 @@ static void SweepSJisJisX0208(const void* profile) { std::uint32_t size = 0; Require(sceCesUcs2ToSJis(ucs2, profile, again, sizeof(again), &size) == 0 && size == 2 && again[0] == cell[0] && again[1] == cell[1]); } else { - Require(result == unassignedCode && used == 77 && ucs2 == 0xEEEE); + Require(result == unassignedCode && used == 2 && ucs2 == 0); } hash = Digest(hash, value); } } - Require(assigned == jisX0208Cells && hash == jisX0208Digest); + Require(assigned == jisX0208Cells && hash == sjisJisX0208Digest); } static void TestJisX0208Profiles(const void* cp932, const void* cp51932) { @@ -155,23 +144,26 @@ static void TestJisX0208Profiles(const void* cp932, const void* cp51932) { Require(EucToUcs2(profile, "\xA1\xF2", 2, ucs2) == 0 && ucs2 == 0x00A3); Require(EucToUcs2(profile, "\xA2\xCC", 2, ucs2) == 0 && ucs2 == 0x00AC); for (const std::uint16_t cp932Form : {0xFF5E, 0x2225, 0xFF0D, 0xFFE0, 0xFFE1, 0xFFE2, 0x2460, 0x2170}) Require(EncodeLeavesOutputs(unassignedCode, profile, cp932Form, false)); - Require(Reports(unsettledCell, [&] { EucEncodes(profile, 0xFF3C, ""); })); - Require(DecodeLeavesOutputs(unassignedCode, profile, "\xAD\xA1", 2, false) && DecodeLeavesOutputs(unassignedCode, profile, "\xF9\xA1", 2, false)); - Require(DecodeLeavesOutputs(unassignedCode, profile, "\xA9\xA1", 2, false) && DecodeLeavesOutputs(unassignedCode, profile, "\xFE\xFE", 2, false)); - Require(DecodeLeavesOutputs(invalidEncode, profile, "\xA4\x41", 2, false) && DecodeLeavesOutputs(invalidEncode, profile, "\xA4\xFF", 2, false)); - Require(DecodeLeavesOutputs(invalidEncode, profile, "\x80", 1, false) && DecodeLeavesOutputs(invalidEncode, profile, "\xFF", 1, false)); - Require(DecodeLeavesOutputs(srcBufferEnd, profile, "\xA4\xA2", 1, false)); + Require(EucToUcs2(profile, "\xA1\xC0", 2, ucs2) == 0 && ucs2 == 0xFF3C && EucEncodes(profile, 0xFF3C, "\xA1\xC0")); + Require(EucToUcs2(profile, "\xA1\xBD", 2, ucs2) == 0 && ucs2 == 0x2014 && EucEncodes(profile, 0x2014, "\xA1\xBD") && EncodeLeavesOutputs(unassignedCode, profile, 0x2015, false)); + Require(EucEncodes(profile, 0x00A5, "\xA1\xEF") && EucEncodes(profile, 0x203E, "\xA1\xB1")); + Require(EucToUcs2(profile, "\xA1\xEF", 2, ucs2) == 0 && ucs2 == 0xFFE5 && EucToUcs2(profile, "\xA1\xB1", 2, ucs2) == 0 && ucs2 == 0xFFE3); + Require(DecodeRejects(unassignedCode, 2, profile, "\xAD\xA1", 2, false) && DecodeRejects(unassignedCode, 2, profile, "\xF9\xA1", 2, false)); + Require(DecodeRejects(unassignedCode, 2, profile, "\xA9\xA1", 2, false) && DecodeRejects(unassignedCode, 2, profile, "\xFE\xFE", 2, false)); + Require(DecodeRejects(invalidEncode, 2, profile, "\xA4\x41", 2, false) && DecodeRejects(invalidEncode, 2, profile, "\xA4\xFF", 2, false)); + Require(DecodeRejects(invalidEncode, 1, profile, "\x80", 1, false) && DecodeRejects(invalidEncode, 1, profile, "\xFF", 1, false)); + Require(DecodeRejects(srcBufferEnd, 2, profile, "\xA4\xA2", 1, false)); std::uint8_t mbcs[4] = {0xEE, 0xEE, 0xEE, 0xEE}; std::uint32_t size = 77; - Require(sceCesUcs2ToEucJp(0x3042, profile, mbcs, 1, &size) == dstBufferEnd && size == 77 && mbcs[0] == 0xEE); + Require(sceCesUcs2ToEucJp(0x3042, profile, mbcs, 1, &size) == dstBufferEnd && size == 2 && mbcs[0] == 0xEE); } Require(EucToUcs2(cp51932, "\xA1\xC0", 2, ucs2) == 0 && ucs2 == 0xFF3C && EucEncodes(cp51932, 0xFF3C, "\xA1\xC0")); Require(EucToUcs2(cp51932, "\xA1\xC1", 2, ucs2) == 0 && ucs2 == 0xFF5E && EucToUcs2(cp51932, "\xAD\xA1", 2, ucs2) == 0 && ucs2 == 0x2460); Require(EucEncodes(cp51932, 0x7E8A, "\xF9\xA1") && EucEncodes(ss3, 0x7E8A, "\x8F\xD4\xE3")); Require(EncodeLeavesOutputs(unassignedCode, euc, 0x7E8A, false) && EncodeLeavesOutputs(unassignedCode, ss2, 0x7E8A, false)); - Require(DecodeLeavesOutputs(invalidEncode, euc, "\x8E\xB1", 2, false) && DecodeLeavesOutputs(invalidEncode, euc, "\x8E", 1, false)); - Require(DecodeLeavesOutputs(invalidEncode, euc, "\x8F\xB0\xA1", 3, false) && DecodeLeavesOutputs(invalidEncode, ss2, "\x8F\xB0\xA1", 3, false)); + Require(DecodeRejects(invalidEncode, 1, euc, "\x8E\xB1", 2, false) && DecodeRejects(invalidEncode, 1, euc, "\x8E", 1, false)); + Require(DecodeRejects(invalidEncode, 1, euc, "\x8F\xB0\xA1", 3, false) && DecodeRejects(invalidEncode, 1, ss2, "\x8F\xB0\xA1", 3, false)); Require(EncodeLeavesOutputs(unassignedCode, euc, 0xFF71, false)); Require(EncodeLeavesOutputs(unassignedCode, euc, 0x4E02, false) && EncodeLeavesOutputs(unassignedCode, ss2, 0x4E02, false)); for (const void* profile : {ss2, ss3}) { @@ -179,8 +171,8 @@ static void TestJisX0208Profiles(const void* cp932, const void* cp51932) { const char bytes[3] = {'\x8E', static_cast(kana), 0}; Require(EucToUcs2(profile, bytes, 2, ucs2) == 0 && ucs2 == 0xFF61 + kana - 0xA1 && EucEncodes(profile, ucs2, bytes)); } - Require(DecodeLeavesOutputs(invalidEncode, profile, "\x8E\xE0", 2, false) && DecodeLeavesOutputs(invalidEncode, profile, "\x8E\xA0", 2, false)); - Require(DecodeLeavesOutputs(srcBufferEnd, profile, "\x8E\xB1", 1, false)); + Require(DecodeRejects(invalidEncode, 2, profile, "\x8E\xE0", 2, false) && DecodeRejects(invalidEncode, 2, profile, "\x8E\xA0", 2, false)); + Require(DecodeRejects(srcBufferEnd, 2, profile, "\x8E\xB1", 1, false)); } std::uint32_t supplementary = 0; std::uint32_t hash = 0x811C9DC5u; @@ -199,7 +191,7 @@ static void TestJisX0208Profiles(const void* cp932, const void* cp51932) { } } Require(supplementary == jisX0212Cells && hash == jisX0212Digest); - Require(DecodeLeavesOutputs(srcBufferEnd, ss3, "\x8F\xB0", 2, false) && DecodeLeavesOutputs(invalidEncode, ss3, "\x8F\xB0\x41", 3, false)); + Require(DecodeRejects(srcBufferEnd, 3, ss3, "\x8F\xB0", 2, false) && DecodeRejects(invalidEncode, 3, ss3, "\x8F\xB0\x41", 3, false)); Require(EucEncodes(ss3, 0x4E02, "\x8F\xB0\xA1") && EucEncodes(ss2, 0xFF71, "\x8E\xB1")); std::uint8_t utf8[4] = {}; @@ -217,30 +209,32 @@ static void TestJisX0208Profiles(const void* cp932, const void* cp51932) { Require(SJisToUcs2(sjis, "\x81\x60", 2, ucs2) == 0 && ucs2 == 0x301C && SJisEncodes(sjis, 0x301C, "\x81\x60")); Require(SJisToUcs2(sjis, "\x81\x91", 2, ucs2) == 0 && ucs2 == 0x00A2 && SJisEncodes(sjis, 0x00AC, "\x81\xCA")); for (const std::uint16_t cp932Form : {0xFF5E, 0x2225, 0xFF0D, 0xFFE0, 0xFFE1, 0xFFE2, 0x2460, 0x2170, 0x7E8A, 0x4E02}) Require(EncodeLeavesOutputs(unassignedCode, sjis, cp932Form, true)); - for (const char* removed : {"\x87\x40", "\xED\x40", "\xEE\xFC", "\xFA\x40", "\xFC\x4B", "\xF0\x40", "\xEF\xFC"}) Require(DecodeLeavesOutputs(unassignedCode, sjis, removed, 2, true)); + for (const char* removed : {"\x87\x40", "\xED\x40", "\xEE\xFC", "\xFA\x40", "\xFC\x4B", "\xF0\x40", "\xEF\xFC"}) Require(DecodeRejects(unassignedCode, 2, sjis, removed, 2, true)); Require(SJisToUcs2(cp932, "\x87\x40", 2, ucs2) == 0 && ucs2 == 0x2460 && SJisToUcs2(cp932, "\xFA\x40", 2, ucs2) == 0 && ucs2 == 0x2170); Require(SJisToUcs2(cp932, "\x81\x5F", 2, ucs2) == 0 && ucs2 == 0xFF3C && SJisToUcs2(cp932, "\\", 1, ucs2) == 0 && ucs2 == 0x5C); - Require(DecodeLeavesOutputs(invalidEncode, sjis, "\x82\x7F", 2, true) && DecodeLeavesOutputs(invalidEncode, sjis, "\x82\xFD", 2, true)); - Require(DecodeLeavesOutputs(srcBufferEnd, sjis, "\x82\xA0", 1, true)); + Require(DecodeRejects(invalidEncode, 2, sjis, "\x82\x7F", 2, true) && DecodeRejects(invalidEncode, 2, sjis, "\x82\xFD", 2, true)); + Require(DecodeRejects(srcBufferEnd, 2, sjis, "\x82\xA0", 1, true)); for (std::uint32_t byte = 0; byte <= 0xFF; ++byte) { const char bytes[2] = {static_cast(byte), 0}; if (byte == 0x5C || byte == 0x7E) { - Require(Reports(unsettledByte, [&] { SJisToUcs2(sjis, bytes, 1, ucs2); })); + const std::uint16_t roman = byte == 0x5C ? 0x00A5 : 0x203E; + Require(SJisToUcs2(sjis, bytes, 1, ucs2) == 0 && ucs2 == roman && SJisEncodes(sjis, roman, bytes)); } else if (byte < 0x80) { Require(SJisToUcs2(sjis, bytes, 1, ucs2) == 0 && ucs2 == byte && (byte == 0 || SJisEncodes(sjis, ucs2, bytes))); } else if (byte >= 0xA1 && byte <= 0xDF) { Require(SJisToUcs2(sjis, bytes, 1, ucs2) == 0 && ucs2 == 0xFF61 + byte - 0xA1 && SJisEncodes(sjis, ucs2, bytes)); } else { - Require(DecodeLeavesOutputs(byte == 0x80 || byte == 0xA0 || byte >= 0xFD ? invalidEncode : srcBufferEnd, sjis, bytes, 1, true)); + Require(DecodeRejects(byte == 0x80 || byte == 0xA0 || byte >= 0xFD ? invalidEncode : srcBufferEnd, byte == 0x80 || byte == 0xA0 || byte >= 0xFD ? 1u : 2u, sjis, bytes, 1, true)); } } - for (const std::uint16_t unsettled : {0x5C, 0x7E, 0xA5, 0x203E}) Require(Reports(unsettledByte, [&] { SJisEncodes(sjis, unsettled, ""); })); - Require(Reports(unsettledCell, [&] { SJisEncodes(sjis, 0xFF3C, ""); })); + Require(SJisToUcs2(sjis, "\x81\x5F", 2, ucs2) == 0 && ucs2 == 0x005C && SJisEncodes(sjis, 0x005C, "\x81\x5F")); + Require(SJisToUcs2(sjis, "\x81\x5C", 2, ucs2) == 0 && ucs2 == 0x2014 && SJisEncodes(sjis, 0x2014, "\x81\x5C")); + for (const std::uint16_t unmapped : {0x007E, 0xFF3C, 0x2015}) Require(EncodeLeavesOutputs(unassignedCode, sjis, unmapped, true)); Require(sceCesMbcsUcsContextInit(context, sjis) == 0); const std::uint16_t text[] = {0x3042, 0x301C, 0xFF71, 0}; std::uint8_t mbcs[8] = {}; Require(sceCesUcs2StrToMbcsStr(context, text, 4, &used, mbcs, sizeof(mbcs), &produced) == 0 && used == 3 && produced == 5 && std::memcmp(mbcs, "\x82\xA0\x81\x60\xB1", 5) == 0); - Require(Reports(unsettledByte, [&] { sceCesMbcsStrToUcs2Str(context, Bytes("A\\"), 2, &used, wide, 8, &produced); })); + Require(sceCesMbcsStrToUcs2Str(context, Bytes("A\\"), 2, &used, wide, 8, &produced) == 0 && used == 2 && produced == 2 && wide[1] == 0x00A5); } int main() { @@ -292,7 +286,7 @@ int main() { Require(sceCesEucJpToUtf16(euc, Bytes("\xA4\xA2"), 2, &consumed, &utf16, 1, &produced) == 0 && consumed == 2 && utf16 == 0x3042); Require(sceCesEucJpToUtf16be(euc, Bytes("\x8E\xB1"), 2, &consumed, &utf16, 1, &produced) == 0); Require(std::memcmp(&utf16, "\xFF\x71", 2) == 0); - Require(sceCesEucJpToUtf32(euc, Bytes("\x8F\xB0\xA1"), 3, &consumed, &utf32) == 0 && consumed == 3 && utf32 == 0x4E02); + Require(sceCesEucJpToUtf32(euc, Bytes("\x8F\xB0\xA1"), 3, &consumed, &utf32) == invalidEncode); Require(sceCesEucJpToUtf32le(euc, Bytes("\xA4\xA2"), 2, &consumed, &utf32) == 0); Require(std::memcmp(&utf32, "\x42\x30\x00\x00", 4) == 0); Require(sceCesEucJpToUtf32(sjis, Bytes("\xA4\xA2"), 2, &consumed, &utf32) == invalidParameter); @@ -301,12 +295,12 @@ int main() { Require(sceCesUcs2ToSJis(0x3042, sjis, mbcs, sizeof(mbcs), &produced) == 0 && produced == 2 && std::memcmp(mbcs, "\x82\xA0", 2) == 0); Require(sceCesUcs2ToSJis(0xFF71, sjis, mbcs, sizeof(mbcs), &produced) == 0 && produced == 1 && mbcs[0] == 0xB1); Require(sceCesUcs2ToSJis(0x3042, sjis, mbcs, 1, &produced) == dstBufferEnd); - Require(sceCesUcs2ToSJis(0xD800, sjis, mbcs, sizeof(mbcs), &produced) == invalidEncode); + Require(sceCesUcs2ToSJis(0xD800, sjis, mbcs, sizeof(mbcs), &produced) == outOfCodeSpace); Require(sceCesUcs2ToSJis(0x4E02, sjis, mbcs, sizeof(mbcs), &produced) == unassignedCode); Require(sceCesUcs2ToSJis(0x3042, euc, mbcs, sizeof(mbcs), &produced) == invalidParameter); Require(sceCesUcs2ToSJis(0x3042, sceCesRefersUcsProfileCp1252(), mbcs, sizeof(mbcs), &produced) == invalidParameter); Require(sceCesUcs2ToSJis(0x3042, sjis, nullptr, sizeof(mbcs), &produced) == invalidDstBuffer); - Require(sceCesUcs2ToEucJp(0x4E02, euc, mbcs, sizeof(mbcs), &produced) == 0 && produced == 3 && std::memcmp(mbcs, "\x8F\xB0\xA1", 3) == 0); + Require(sceCesUcs2ToEucJp(0x4E02, euc, mbcs, sizeof(mbcs), &produced) == unassignedCode); Require(sceCesUcs2ToEucJp(0xFF71, euc, mbcs, sizeof(mbcs), &produced) == 0 && produced == 2 && std::memcmp(mbcs, "\x8E\xB1", 2) == 0); Require(sceCesUtf8ToSJis(Bytes("\xE6\x97\xA5"), 3, &consumed, sjis, mbcs, sizeof(mbcs), &produced) == 0); @@ -338,8 +332,10 @@ int main() { std::memcpy(&utf32Le, "\xE5\x65\x00\x00", 4); Require(sceCesUtf32leToEucJp(&utf32Le, euc, mbcs, sizeof(mbcs), &produced) == 0 && std::memcmp(mbcs, "\xC6\xFC", 2) == 0); + constexpr int microsoftForms[] = {0x00A2, 0x00A3, 0x00A5, 0x00AC, 0x2014, 0x2016, 0x203E, 0x2212, 0x301C, 0xFEFF}; for (int code = 1; code < 0x10000; ++code) { if (code >= 0xD800 && code <= 0xDFFF) continue; + if (std::find(std::begin(microsoftForms), std::end(microsoftForms), code) != std::end(microsoftForms)) continue; std::uint8_t encoded[4] = {}; std::uint32_t size = 0; if (sceCesUcs2ToSJis(static_cast(code), sjis, encoded, sizeof(encoded), &size) == 0) { diff --git a/core/libs/tests/GuestCesMbc.cpp b/core/libs/tests/GuestCesMbc.cpp index 4bab69f25e..9d1e17c780 100644 --- a/core/libs/tests/GuestCesMbc.cpp +++ b/core/libs/tests/GuestCesMbc.cpp @@ -1,13 +1,232 @@ #include "prx/libSceCesCs/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include +constexpr int invalidParameter = static_cast(0x805C0001); +constexpr int invalidSrcBuffer = static_cast(0x805C0010); +constexpr int srcBufferEnd = static_cast(0x805C0011); +constexpr int invalidEncode = static_cast(0x805C0014); +constexpr int outOfCodeSpace = static_cast(0x805C0015); +constexpr int unassignedCode = static_cast(0x805C0020); +constexpr int unsupported = static_cast(0x805C0024); +constexpr int invalidDstBuffer = static_cast(0x805C0030); +constexpr int dstBufferEnd = static_cast(0x805C0031); +constexpr std::uint32_t untouched = 0xEEEEEEEE; + +enum Form { Ucs2, Utf8, Utf16, Utf16be, Utf16le, Utf32, Utf32be, Utf32le, FormCount }; + +struct Sequence { + std::uint32_t count; + std::uint32_t units[4]; + int expected; +}; + static void Require(bool value) { if (!value) std::abort(); } static const std::uint8_t* Bytes(const char* text) { return reinterpret_cast(text); } +static bool IsBuffer(int form) { return form == Utf8 || form == Utf16 || form == Utf16be || form == Utf16le; } + +static std::uint32_t Units(int form, std::uint32_t code, std::uint32_t* units) { + constexpr std::uint32_t leads[5] = {0, 0, 0xC0, 0xE0, 0xF0}; + if (form == Utf8 && code >= 0x80) { + const std::uint32_t count = code < 0x800 ? 2 : code < 0x10000 ? 3 : 4; + for (std::uint32_t i = count - 1; i > 0; --i, code >>= 6) units[i] = 0x80 | (code & 0x3F); + units[0] = leads[count] | code; + return count; + } + if (IsBuffer(form) && code >= 0x10000) { + units[0] = 0xD800 + ((code - 0x10000) >> 10); + units[1] = 0xDC00 + ((code - 0x10000) & 0x3FF); + return 2; + } + units[0] = code; + return 1; +} + +static int Call(int form, const std::uint32_t* units, std::uint32_t count, const void* profile, std::uint8_t* mbc, std::uint32_t mbcMax, std::uint32_t* mbcLen, std::uint32_t* sourceLen) { + const bool bigEndian = form == Utf16be || form == Utf32be || ((form == Utf16 || form == Utf32) && std::endian::native == std::endian::big); + const std::uint32_t width = form == Utf8 ? 1 : form >= Utf32 ? 4 : 2; + alignas(4) std::uint8_t bytes[16] = {}; + for (std::uint32_t i = 0; units && i < count; ++i) { + for (std::uint32_t b = 0; b < width; ++b) bytes[i * width + b] = static_cast(units[i] >> (8 * (bigEndian ? width - 1 - b : b))); + } + const std::uint8_t* utf8 = units ? bytes : nullptr; + const auto* utf16 = reinterpret_cast(utf8); + const auto* utf32 = reinterpret_cast(utf8); + const std::uint32_t value = units ? units[0] : 0; + switch (form) { + case Ucs2: return sceCesUcs2ToMbc(static_cast(value), profile, mbc, mbcMax, mbcLen); + case Utf8: return sceCesUtf8ToMbc(utf8, count, sourceLen, profile, mbc, mbcMax, mbcLen); + case Utf16: return sceCesUtf16ToMbc(utf16, count, sourceLen, profile, mbc, mbcMax, mbcLen); + case Utf16be: return sceCesUtf16beToMbc(utf16, count, sourceLen, profile, mbc, mbcMax, mbcLen); + case Utf16le: return sceCesUtf16leToMbc(utf16, count, sourceLen, profile, mbc, mbcMax, mbcLen); + case Utf32: return sceCesUtf32ToMbc(value, profile, mbc, mbcMax, mbcLen); + case Utf32be: return sceCesUtf32beToMbc(utf32, profile, mbc, mbcMax, mbcLen); + default: return sceCesUtf32leToMbc(utf32, profile, mbc, mbcMax, mbcLen); + } +} + +static void RequireFailure(int form, const std::uint32_t* units, std::uint32_t count, const void* profile, bool target, std::uint32_t mbcMax, int expected) { + std::uint8_t mbc[4] = {0xEE, 0xEE, 0xEE, 0xEE}; + std::uint32_t mbcLen = untouched; + std::uint32_t sourceLen = untouched; + Require(Call(form, units, count, profile, target ? mbc : nullptr, mbcMax, &mbcLen, &sourceLen) == expected); + const bool refused = !IsBuffer(form) && expected != invalidParameter && expected != invalidSrcBuffer && expected != invalidDstBuffer; + Require(mbc[0] == 0xEE && mbc[1] == 0xEE && mbc[2] == 0xEE && mbc[3] == 0xEE && mbcLen == (refused ? 0 : untouched) && sourceLen == untouched); +} + +static void RequireAll(std::uint32_t code, const void* profile, int expected, const std::uint8_t* bytes, std::uint32_t size) { + for (int form = code > 0xFFFF ? Utf8 : Ucs2; form < FormCount; ++form) { + std::uint32_t units[4] = {}; + const std::uint32_t count = Units(form, code, units); + if (expected != 0) { + RequireFailure(form, units, count, profile, true, 4, expected); + continue; + } + std::uint8_t mbc[4] = {0xEE, 0xEE, 0xEE, 0xEE}; + std::uint32_t mbcLen = untouched; + std::uint32_t sourceLen = untouched; + Require(Call(form, units, count, profile, mbc, sizeof(mbc), &mbcLen, &sourceLen) == 0); + Require(mbcLen == size && std::memcmp(mbc, bytes, size) == 0 && sourceLen == (IsBuffer(form) ? count : untouched)); + } +} + +static std::uint32_t SweepCodes(const void* profile) { + constexpr std::uint32_t supplementary[] = {0x10000, 0x1F600, 0x10FFFF}; + std::uint32_t mapped = 0; + for (std::uint32_t code = 0; code < 0x10000; ++code) { + if (code >= 0xD800 && code <= 0xDFFF) continue; + std::uint8_t bytes[4] = {}; + std::uint32_t size = 0; + const int result = sceCesUtf32ToMbc(code, profile, bytes, sizeof(bytes), &size); + Require(result == 0 || result == unassignedCode || result == unsupported); + if (result == 0 && size > 0) { + std::uint32_t consumed = 0; + std::uint32_t back = 0; + Require(sceCesMbcToUtf32(profile, bytes, size, &consumed, &back) == 0 && consumed == size); + if (back == code) ++mapped; + } + RequireAll(code, profile, result, bytes, size); + } + for (const std::uint32_t code : supplementary) RequireAll(code, profile, unassignedCode, nullptr, 0); + return mapped; +} + +static std::uint32_t RoundTrip(const void* profile, const std::uint8_t* source, std::uint32_t size) { + std::uint32_t consumed = 0; + std::uint32_t produced = 0; + std::uint32_t code = 0; + std::uint16_t ucs2 = 0; + std::uint8_t utf8[4] = {}; + std::uint32_t utf8Size = 0; + std::uint16_t utf16[3] = {}; + std::uint32_t utf32[2] = {}; + Require(sceCesMbcToUtf32(profile, source, size, &consumed, &code) == 0 && consumed == size); + Require(sceCesMbcToUcs2(profile, source, size, &consumed, &ucs2) == 0 && consumed == size); + Require(sceCesMbcToUtf8(profile, source, size, &consumed, utf8, sizeof(utf8), &utf8Size) == 0 && consumed == size); + Require(sceCesMbcToUtf16(profile, source, size, &consumed, &utf16[0], 1, &produced) == 0 && consumed == size); + Require(sceCesMbcToUtf16be(profile, source, size, &consumed, &utf16[1], 1, &produced) == 0 && consumed == size); + Require(sceCesMbcToUtf16le(profile, source, size, &consumed, &utf16[2], 1, &produced) == 0 && consumed == size); + Require(sceCesMbcToUtf32be(profile, source, size, &consumed, &utf32[0]) == 0 && consumed == size); + Require(sceCesMbcToUtf32le(profile, source, size, &consumed, &utf32[1]) == 0 && consumed == size); + std::uint8_t mbc[8][4] = {}; + std::uint32_t mbcLen[8] = {}; + std::uint32_t sourceLen[4] = {}; + Require(sceCesUtf32ToMbc(code, profile, mbc[0], 4, &mbcLen[0]) == 0); + Require(sceCesUcs2ToMbc(ucs2, profile, mbc[1], 4, &mbcLen[1]) == 0); + Require(sceCesUtf8ToMbc(utf8, utf8Size, &sourceLen[0], profile, mbc[2], 4, &mbcLen[2]) == 0 && sourceLen[0] == utf8Size); + Require(sceCesUtf16ToMbc(&utf16[0], 1, &sourceLen[1], profile, mbc[3], 4, &mbcLen[3]) == 0 && sourceLen[1] == 1); + Require(sceCesUtf16beToMbc(&utf16[1], 1, &sourceLen[2], profile, mbc[4], 4, &mbcLen[4]) == 0 && sourceLen[2] == 1); + Require(sceCesUtf16leToMbc(&utf16[2], 1, &sourceLen[3], profile, mbc[5], 4, &mbcLen[5]) == 0 && sourceLen[3] == 1); + Require(sceCesUtf32beToMbc(&utf32[0], profile, mbc[6], 4, &mbcLen[6]) == 0); + Require(sceCesUtf32leToMbc(&utf32[1], profile, mbc[7], 4, &mbcLen[7]) == 0); + for (std::uint32_t i = 1; i < 8; ++i) Require(mbcLen[i] == mbcLen[0] && std::memcmp(mbc[i], mbc[0], mbcLen[0]) == 0); + return mbcLen[0] == size && std::memcmp(mbc[0], source, size) == 0 ? 1 : 0; +} + +static std::uint32_t SweepBytes(const void* profile) { + std::uint32_t identical = 0; + std::uint8_t source[3] = {}; + std::uint32_t consumed = 0; + std::uint32_t code = 0; + for (std::uint32_t lead = 0; lead < 256; ++lead) { + source[0] = static_cast(lead); + const int single = sceCesMbcToUtf32(profile, source, 1, &consumed, &code); + if (single == 0) identical += RoundTrip(profile, source, 1); + if (single != srcBufferEnd) continue; + for (std::uint32_t trail = 0; trail < 256; ++trail) { + source[1] = static_cast(trail); + const int pair = sceCesMbcToUtf32(profile, source, 2, &consumed, &code); + if (pair == 0) identical += RoundTrip(profile, source, 2); + for (std::uint32_t third = 0; pair == srcBufferEnd && third < 256; ++third) { + source[2] = static_cast(third); + if (sceCesMbcToUtf32(profile, source, 3, &consumed, &code) == 0) identical += RoundTrip(profile, source, 3); + } + } + } + return identical; +} + +static void RequireInvalid(const void* profile) { + constexpr int scalarForms[] = {Ucs2, Utf32, Utf32be, Utf32le}; + const Sequence utf8[] = { + {2, {0xC0, 0x80}, outOfCodeSpace}, {2, {0xC1, 0xBF}, outOfCodeSpace}, {3, {0xE0, 0x80, 0x80}, outOfCodeSpace}, + {3, {0xE0, 0x9F, 0xBF}, outOfCodeSpace}, {4, {0xF0, 0x80, 0x80, 0x80}, outOfCodeSpace}, {4, {0xF0, 0x8F, 0xBF, 0xBF}, outOfCodeSpace}, + {3, {0xED, 0xA0, 0x80}, outOfCodeSpace}, {3, {0xED, 0xBF, 0xBF}, outOfCodeSpace}, {4, {0xF4, 0x90, 0x80, 0x80}, unassignedCode}, + {4, {0xF7, 0xBF, 0xBF, 0xBF}, unassignedCode}, {1, {0x80}, invalidEncode}, {1, {0xFF}, invalidEncode}, + {3, {0xE3, 0x41, 0x42}, invalidEncode}, {2, {0xE3, 0x81}, srcBufferEnd}, {3, {0xF0, 0x9F, 0x98}, srcBufferEnd}, + }; + const Sequence utf16[] = { + {1, {0xDC00}, invalidEncode}, {1, {0xDFFF}, invalidEncode}, {2, {0xD800, 0x0041}, invalidEncode}, + {2, {0xDBFF, 0xDBFF}, invalidEncode}, {2, {0xDC00, 0xD800}, invalidEncode}, {1, {0xD800}, srcBufferEnd}, + }; + const Sequence utf32[] = {{1, {0xD800}, outOfCodeSpace}, {1, {0xDFFF}, outOfCodeSpace}, {1, {0x110000}, invalidEncode}, {1, {0xFFFFFFFF}, invalidEncode}}; + for (const Sequence& sequence : utf8) RequireFailure(Utf8, sequence.units, sequence.count, profile, true, 4, sequence.expected); + for (const Sequence& sequence : utf16) { + for (int form = Utf16; form <= Utf16le; ++form) RequireFailure(form, sequence.units, sequence.count, profile, true, 4, sequence.expected); + } + for (const Sequence& sequence : utf32) { + for (const int form : scalarForms) { + if (form != Ucs2 || sequence.units[0] <= 0xFFFF) RequireFailure(form, sequence.units, sequence.count, profile, true, 4, sequence.expected); + } + } +} + +struct Rejected { + const void* profile; + const char* bytes; + std::uint32_t size; + int expected; + std::uint32_t length; +}; + +static void RequireRejected(const Rejected& sequence) { + for (int form = 0; form < 8; ++form) { + std::uint32_t consumed = 0xEEEEEEEE; + std::uint32_t produced = 0; + std::uint16_t units[2] = {}; + std::uint8_t utf8[4] = {}; + std::uint32_t utf32 = 0; + const auto* bytes = Bytes(sequence.bytes); + int result = 0; + switch (form) { + case 0: result = sceCesMbcToUcs2(sequence.profile, bytes, sequence.size, &consumed, units); break; + case 1: result = sceCesMbcToUtf8(sequence.profile, bytes, sequence.size, &consumed, utf8, sizeof(utf8), &produced); break; + case 2: result = sceCesMbcToUtf16(sequence.profile, bytes, sequence.size, &consumed, units, 2, &produced); break; + case 3: result = sceCesMbcToUtf16be(sequence.profile, bytes, sequence.size, &consumed, units, 2, &produced); break; + case 4: result = sceCesMbcToUtf16le(sequence.profile, bytes, sequence.size, &consumed, units, 2, &produced); break; + case 5: result = sceCesMbcToUtf32(sequence.profile, bytes, sequence.size, &consumed, &utf32); break; + case 6: result = sceCesMbcToUtf32be(sequence.profile, bytes, sequence.size, &consumed, &utf32); break; + default: result = sceCesMbcToUtf32le(sequence.profile, bytes, sequence.size, &consumed, &utf32); break; + } + Require(result == sequence.expected && consumed == sequence.length); + } +} + static void RequireX0213(std::uint16_t sjis, std::uint8_t men, std::uint8_t ku, std::uint8_t ten) { std::uint8_t m = 0; std::uint8_t k = 0; @@ -18,14 +237,6 @@ static void RequireX0213(std::uint16_t sjis, std::uint8_t men, std::uint8_t ku, } int main() { - constexpr int invalidParameter = static_cast(0x805C0001); - constexpr int invalidSrcBuffer = static_cast(0x805C0010); - constexpr int srcBufferEnd = static_cast(0x805C0011); - constexpr int invalidEncode = static_cast(0x805C0014); - constexpr int unassignedCode = static_cast(0x805C0020); - constexpr int invalidDstBuffer = static_cast(0x805C0030); - constexpr int dstBufferEnd = static_cast(0x805C0031); - alignas(8) std::uint8_t sjisSheet[64] = {}; alignas(8) std::uint8_t eucSheet[64] = {}; alignas(8) std::uint8_t gbkSheet[64] = {}; @@ -48,9 +259,25 @@ int main() { Require(sceCesMbcToUcs2(sjis, nullptr, 1, &consumed, &ucs2) == invalidSrcBuffer); Require(sceCesMbcToUcs2(sjis, Bytes("A"), 0, &consumed, &ucs2) == srcBufferEnd); Require(sceCesMbcToUcs2(sjis, Bytes("A"), 1, &consumed, nullptr) == invalidDstBuffer); - Require(sceCesMbcToUcs2(sjis, Bytes("\x82"), 1, &consumed, &ucs2) == srcBufferEnd); - Require(sceCesMbcToUcs2(sjis, Bytes("\x80"), 1, &consumed, &ucs2) == invalidEncode); - Require(sceCesMbcToUcs2(sjis, Bytes("\xF0\x40"), 2, &consumed, &ucs2) == unassignedCode); + + alignas(8) std::uint8_t gb18030Sheet[64] = {}; + alignas(8) std::uint8_t uhcSheet[64] = {}; + alignas(8) std::uint8_t big5Sheet[64] = {}; + const void* gb18030 = sceCesUcsProfileInitGb18030_2000(gb18030Sheet); + const void* uhc = sceCesUcsProfileInitUhc(uhcSheet); + const void* big5 = sceCesUcsProfileInitBig5Cp950(big5Sheet); + const Rejected rejected[] = { + {sjis, "\x82", 1, srcBufferEnd, 2}, {sjis, "\x80", 1, invalidEncode, 1}, {sjis, "\xA0", 1, invalidEncode, 1}, + {sjis, "\x82\x00", 2, invalidEncode, 1}, {sjis, "\x82\x7F", 2, invalidEncode, 2}, {sjis, "\xF0\x40", 2, unassignedCode, 2}, + {euc, "\xA4", 1, srcBufferEnd, 2}, {euc, "\x8F\xA1", 2, srcBufferEnd, 3}, {euc, "\xFF", 1, invalidEncode, 1}, + {euc, "\xA4\x00", 2, invalidEncode, 1}, {euc, "\xA4\x41", 2, invalidEncode, 2}, {euc, "\x8F\x00", 2, invalidEncode, 1}, + {gbk, "\x81", 1, srcBufferEnd, 2}, {gbk, "\xFF", 1, invalidEncode, 1}, {gbk, "\x81\x00", 2, invalidEncode, 1}, {gbk, "\x81\x7F", 2, invalidEncode, 2}, + {gb18030, "\x81\x30", 2, srcBufferEnd, 4}, {gb18030, "\x81\x30\x81", 3, srcBufferEnd, 4}, {gb18030, "\x81\x00", 2, invalidEncode, 1}, + {gb18030, "\x81\x30\x00", 3, invalidEncode, 2}, {gb18030, "\x81\x30\x20\x00", 4, invalidEncode, 3}, {gb18030, "\x81\x30\x20\x30", 4, invalidEncode, 4}, + {uhc, "\xC9", 1, srcBufferEnd, 2}, {uhc, "\xC9\xFF", 2, invalidEncode, 2}, {uhc, "\xC9\x41", 2, unassignedCode, 2}, + {big5, "\xA1\x00", 2, invalidEncode, 1}, {big5, "\xA1\x7F", 2, invalidEncode, 2}, {big5, "\x81\x40", 2, unassignedCode, 2}, + }; + for (const Rejected& sequence : rejected) RequireRejected(sequence); std::uint8_t utf8[4] = {}; Require(sceCesMbcToUtf8(gbk, Bytes("\xD6\xD0"), 2, &consumed, utf8, sizeof(utf8), &produced) == 0 && consumed == 2 && produced == 3); @@ -132,4 +359,76 @@ int main() { Require(std::memcmp(utf8, "\xE4\xB8\xAD", 3) == 0); Require(sceCesMbcsUcsContextInitCopy(nullptr, context) == invalidParameter); Require(sceCesMbcsUcsContextInitCopy(copy, nullptr) == invalidParameter); + + alignas(8) std::uint8_t eucCnSheet[64] = {}; + alignas(8) std::uint8_t eucKrSheet[64] = {}; + const void* eucCn = sceCesUcsProfileInitEucCnGb2312(eucCnSheet); + const void* eucKr = sceCesUcsProfileInitEucKr(eucKrSheet); + const void* cp1251 = sceCesRefersUcsProfileCp1251(); + const void* cp1250 = sceCesRefersUcsProfileCp1250(); + + RequireAll(0x20AC, cp1252, 0, Bytes("\x80"), 1); + RequireAll(0x00E9, cp1252, 0, Bytes("\xE9"), 1); + RequireAll(0x0100, cp1252, unassignedCode, nullptr, 0); + RequireAll(0x0416, cp1251, 0, Bytes("\xC6"), 1); + RequireAll(0x0141, cp1250, 0, Bytes("\xA3"), 1); + RequireAll(0x3042, sjis, 0, Bytes("\x82\xA0"), 2); + RequireAll(0xFF71, sjis, 0, Bytes("\xB1"), 1); + RequireAll(0x4E02, sjis, unassignedCode, nullptr, 0); + RequireAll(0x3042, euc, 0, Bytes("\xA4\xA2"), 2); + RequireAll(0xFF71, euc, 0, Bytes("\x8E\xB1"), 2); + RequireAll(0x4E02, euc, unassignedCode, nullptr, 0); + RequireAll(0x4E2D, gbk, 0, Bytes("\xD6\xD0"), 2); + RequireAll(0x4E02, gbk, 0, Bytes("\x81\x40"), 2); + RequireAll(0x20AC, gbk, 0, Bytes("\x80"), 1); + RequireAll(0x4E2D, eucCn, 0, Bytes("\xD6\xD0"), 2); + RequireAll(0x4E02, eucCn, unassignedCode, nullptr, 0); + RequireAll(0xAC00, eucKr, 0, Bytes("\xB0\xA1"), 2); + RequireAll(0xAC02, eucKr, unassignedCode, nullptr, 0); + RequireAll(0xAC02, uhc, 0, Bytes("\x81\x41"), 2); + RequireAll(0x4E00, big5, 0, Bytes("\xA4\x40"), 2); + + std::uint32_t units[4] = {}; + for (int form = Ucs2; form < FormCount; ++form) { + std::uint32_t count = Units(form, 0xDC00, units); + RequireFailure(form, nullptr, 0, nullptr, false, 0, invalidParameter); + RequireFailure(form, units, count, blankSheet, true, 4, invalidParameter); + if (form != Ucs2 && form != Utf32) RequireFailure(form, nullptr, 0, sjis, false, 0, invalidSrcBuffer); + if (IsBuffer(form)) RequireFailure(form, units, 0, sjis, false, 0, srcBufferEnd); + RequireFailure(form, units, count, sjis, false, 0, invalidDstBuffer); + RequireFailure(form, units, count, sjis, true, 0, IsBuffer(form) && form != Utf8 ? invalidEncode : outOfCodeSpace); + count = Units(form, 0x4E02, units); + RequireFailure(form, units, count, sjis, true, 0, unassignedCode); + count = Units(form, 0x3042, units); + std::uint8_t full[4] = {0xEE, 0xEE, 0xEE, 0xEE}; + std::uint32_t needed = untouched; + std::uint32_t sourceLen = untouched; + Require(Call(form, units, count, sjis, full, 1, &needed, &sourceLen) == dstBufferEnd && needed == (IsBuffer(form) ? untouched : 2) && full[0] == 0xEE && sourceLen == untouched); + std::uint8_t mbc[4] = {}; + Require(Call(form, units, count, sjis, mbc, sizeof(mbc), nullptr, nullptr) == 0 && std::memcmp(mbc, "\x82\xA0", 2) == 0); + } + + std::uint8_t mbc[4] = {}; + std::uint32_t wide = 0; + std::memcpy(&wide, "\x00\x00\x30\x42", 4); + Require(sceCesUtf32beToMbc(&wide, sjis, mbc, sizeof(mbc), &produced) == 0 && produced == 2 && std::memcmp(mbc, "\x82\xA0", 2) == 0); + Require(sceCesUtf32leToMbc(&wide, sjis, mbc, sizeof(mbc), &produced) == invalidEncode); + std::uint16_t narrow = 0; + std::memcpy(&narrow, "\xD8\x00", 2); + Require(sceCesUtf16leToMbc(&narrow, 1, &consumed, cp1252, mbc, sizeof(mbc), &produced) == 0 && consumed == 1 && produced == 1 && mbc[0] == 0xD8); + Require(sceCesUtf16beToMbc(&narrow, 1, &consumed, cp1252, mbc, sizeof(mbc), &produced) == srcBufferEnd); + Require(sceCesUtf8ToMbc(Bytes("A\xE4\xB8\xAD"), 4, &consumed, gbk, mbc, sizeof(mbc), &produced) == 0 && consumed == 1 && produced == 1 && mbc[0] == 'A'); + + const void* singleByte[] = {cp1252, cp1251, cp1250}; + constexpr std::uint32_t singleByteMapped[] = {251, 255, 251}; + const void* multiByte[] = {sjis, euc, eucCn, gbk, eucKr, uhc, big5}; + for (std::size_t index = 0; index < 3; ++index) { + Require(SweepCodes(singleByte[index]) == singleByteMapped[index] && SweepBytes(singleByte[index]) == singleByteMapped[index]); + RequireInvalid(singleByte[index]); + } + for (const void* profile : multiByte) { + const std::uint32_t mapped = SweepCodes(profile); + Require(mapped > 256 && SweepBytes(profile) == mapped); + RequireInvalid(profile); + } } diff --git a/core/libs/tests/GuestCesUnicode.cpp b/core/libs/tests/GuestCesUnicode.cpp index 75a78e85bc..fa8fee7f88 100644 --- a/core/libs/tests/GuestCesUnicode.cpp +++ b/core/libs/tests/GuestCesUnicode.cpp @@ -1,7 +1,6 @@ #include "prx/libSceCesCs/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libSceCesCs/JisX0208.hpp" -#include "prx/libSceCesCs/JisX0212.hpp" #include #include #include @@ -160,6 +159,7 @@ void TestErrors() { } void TestEucJp() { + constexpr int INVALID_PROFILE = static_cast(0x805C0004); std::array sheet{}; const void* profile = sceCesUcsProfileInitEucJpCp51932(sheet.data()); auto check = [&](const uint8_t* source, uint32_t length, uint32_t code) { @@ -177,10 +177,11 @@ void TestEucJp() { output.fill(0xA5); consumed = produced = SENTINEL; Require(sceCesEucJpToUtf8(profile, source, length, &consumed, output.data(), cap, &produced) == DST_BUFFER_END); - Require(consumed == SENTINEL && produced == SENTINEL && output[0] == 0xA5); + Require(consumed == length && produced == expectedLength && output[0] == 0xA5); } }; - for (uint32_t code = 0; code < 128; ++code) { + for (uint32_t code = 0; code < 0xA0; ++code) { + if (code == 0x0E || code == 0x8E || code == 0x8F) continue; const uint8_t source[] = {static_cast(code)}; check(source, 1, code); } @@ -189,39 +190,41 @@ void TestEucJp() { check(source, 2, 0xFF61 + cell); } uint32_t assigned = 0; - for (uint32_t supplementary = 0; supplementary < 2; ++supplementary) { - for (uint32_t cell = 0; cell < 94 * 94; ++cell) { - const uint32_t code = supplementary ? JisX0212::TO_UCS2[cell] : JisX0208::TO_UCS2[cell]; - const uint8_t source[] = {0x8F, static_cast(0xA1 + cell / 94), static_cast(0xA1 + cell % 94)}; - const uint8_t* start = source + (supplementary ? 0 : 1); - const uint32_t length = supplementary ? 3 : 2; - if (code) { check(start, length, code); ++assigned; } - else { - uint8_t output[4] = {0xA5, 0xA5, 0xA5, 0xA5}; - uint32_t consumed = SENTINEL; - uint32_t produced = SENTINEL; - Require(sceCesEucJpToUtf8(profile, start, length, &consumed, output, 4, &produced) == UNASSIGNED_CODE); - Require(consumed == SENTINEL && produced == SENTINEL && output[0] == 0xA5); - } + for (uint32_t cell = 0; cell < 94 * 94; ++cell) { + const uint32_t code = JisX0208::TO_UCS2[cell]; + const uint8_t source[] = {static_cast(0xA1 + cell / 94), static_cast(0xA1 + cell % 94)}; + if (code) { check(source, 2, code); ++assigned; } + else { + uint8_t output[4] = {0xA5, 0xA5, 0xA5, 0xA5}; + uint32_t consumed = SENTINEL; + uint32_t produced = SENTINEL; + Require(sceCesEucJpToUtf8(profile, source, 2, &consumed, output, 4, &produced) == UNASSIGNED_CODE); + Require(consumed == 2 && produced == 0 && output[0] == 0xA5); } } - Require(assigned == 13403); - const uint8_t source[] = {0x8F, 0xA2, 0xAF, 'X'}; + Require(assigned == 7336); + const uint8_t source[] = {0xA4, 0xA2, 'X'}; uint8_t output[4] = {0xA5, 0xA5, 0xA5, 0xA5}; uint32_t consumed = SENTINEL; uint32_t produced = SENTINEL; - Require(sceCesEucJpToUtf8(nullptr, source, 3, &consumed, output, 4, &produced) == INVALID_PARAMETER); + Require(sceCesEucJpToUtf8(nullptr, source, 3, &consumed, output, 4, &produced) == INVALID_PROFILE); + Require(consumed == 0 && produced == 0); Require(sceCesEucJpToUtf8(profile, nullptr, 3, &consumed, output, 4, &produced) == INVALID_SRC_BUFFER); - Require(sceCesEucJpToUtf8(profile, source, 0, &consumed, output, 4, &produced) == SRC_BUFFER_END); + Require(sceCesEucJpToUtf8(profile, source, 0, &consumed, output, 4, &produced) == INVALID_SRC_BUFFER); Require(sceCesEucJpToUtf8(profile, source, 3, &consumed, nullptr, 4, &produced) == INVALID_DST_BUFFER); - for (uint32_t size : {1u, 2u}) Require(sceCesEucJpToUtf8(profile, source, size, &consumed, output, 4, &produced) == SRC_BUFFER_END); + Require(consumed == 2 && produced == 3); + Require(sceCesEucJpToUtf8(profile, source, 1, &consumed, output, 4, &produced) == SRC_BUFFER_END); + Require(consumed == 2 && produced == 0); const uint8_t malformed[] = {0x8E, 0x41}; Require(sceCesEucJpToUtf8(profile, malformed, 2, &consumed, output, 4, &produced) == INVALID_ENCODE); - Require(consumed == SENTINEL && produced == SENTINEL && output[0] == 0xA5); - Require(sceCesEucJpToUtf8(profile, source, 4, &consumed, output, 4, &produced) == 0); - Require(consumed == 3 && produced == 2 && output[0] == 0xCB && output[1] == 0x98 && output[2] == 0xA5); - Require(sceCesEucJpToUtf8(profile, source, 4, nullptr, output, 4, nullptr) == 0); - std::puts("13403 assigned JIS cells plus ASCII, half-width katakana and unassigned cells checked"); + Require(consumed == 2 && produced == 0 && output[0] == 0xA5); + const uint8_t supplementary[] = {0x8F, 0xA2, 0xAF}; + Require(sceCesEucJpToUtf8(profile, supplementary, 3, &consumed, output, 4, &produced) == INVALID_ENCODE); + Require(consumed == 3 && produced == 0 && output[0] == 0xA5); + Require(sceCesEucJpToUtf8(profile, source, 3, &consumed, output, 4, &produced) == 0); + Require(consumed == 2 && produced == 3 && output[0] == 0xE3 && output[1] == 0x81 && output[2] == 0x82 && output[3] == 0xA5); + Require(sceCesEucJpToUtf8(profile, source, 3, nullptr, output, 4, nullptr) == 0); + std::puts("7336 assigned JIS X 0208 cells plus single bytes and half-width katakana checked"); } } diff --git a/core/libs/tests/GuestCesWide.cpp b/core/libs/tests/GuestCesWide.cpp index bf4f34bd30..e644f825e5 100644 --- a/core/libs/tests/GuestCesWide.cpp +++ b/core/libs/tests/GuestCesWide.cpp @@ -68,6 +68,10 @@ int main() { Require(sceCesUtf32leToSbc(&yLe, cp1252, &sbc) == 0 && sbc == 0x9F); Require(sceCesUtf32leToSbc(nullptr, cp1252, &sbc) == invalidSrcBuffer); for (int value = 0; value < 256; ++value) { + if (value == 0x98) { + Require(sceCesSbcToUtf32(cp1251, static_cast(value), &utf32) == unassignedCode); + continue; + } Require(sceCesSbcToUtf32(cp1251, static_cast(value), &utf32) == 0); Require(sceCesUtf32ToSbc(utf32, cp1251, &sbc) == 0 && sbc == value); } diff --git a/core/libs/tests/GuestCharacter.cpp b/core/libs/tests/GuestCharacter.cpp index c6fc299ba0..7f900acea7 100644 --- a/core/libs/tests/GuestCharacter.cpp +++ b/core/libs/tests/GuestCharacter.cpp @@ -8,7 +8,47 @@ #include static void Require(bool value) { if (!value) std::abort(); } +static int ConsoleClass(int c) { + if (c < 0 || c > 255) return 0; + int bits = 0; + if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) bits |= 0x01; + if (c >= 'A' && c <= 'Z') bits |= 0x02; + if (c == ' ') bits |= 0x04; + if (c >= 33 && c <= 126 && !(c >= '0' && c <= '9') && !(c >= 'A' && c <= 'Z') && !(c >= 'a' && c <= 'z')) bits |= 0x08; + if (c >= 'a' && c <= 'z') bits |= 0x10; + if (c >= '0' && c <= '9') bits |= 0x20; + if (c >= '\t' && c <= '\r') bits |= 0x40; + if (c < 32 || c == 127) bits |= 0x80; + if (c == '\t') bits |= 0x400; + return bits; +} +static void CheckConsoleClassValues() { + Require(isspace_nid_postfix(' ') == 0x04 && isspace_nid_postfix('\t') == 0x40 && isspace_nid_postfix('\n') == 0x40); + Require(isblank_nid_postfix(' ') == 0x04 && isblank_nid_postfix('\t') == 0x400 && iscntrl_nid_postfix('\t') == 0x80); + Require(isdigit_nid_postfix('0') == 0x20 && isalnum_nid_postfix('0') == 0x20 && isxdigit_nid_postfix('0') == 0x01); + Require(isupper_nid_postfix('A') == 0x02 && isalpha_nid_postfix('A') == 0x02 && isxdigit_nid_postfix('F') == 0x01 && isxdigit_nid_postfix('G') == 0); + Require(islower_nid_postfix('a') == 0x10 && isalpha_nid_postfix('a') == 0x10 && isprint_nid_postfix(' ') == 0x04 && isgraph_nid_postfix(' ') == 0); + Require(ispunct_nid_postfix('_') == 0x08 && isgraph_nid_postfix('_') == 0x08 && isprint_nid_postfix('_') == 0x08 && iscntrl_nid_postfix(127) == 0x80); + for (int c = EOF; c <= 255; ++c) { + const int bits = ConsoleClass(c); + Require(isupper_nid_postfix(c) == (bits & 0x02) && islower_nid_postfix(c) == (bits & 0x10) && isalpha_nid_postfix(c) == (bits & 0x12)); + Require(isdigit_nid_postfix(c) == (bits & 0x20) && isalnum_nid_postfix(c) == (bits & 0x32) && isxdigit_nid_postfix(c) == (bits & 0x01)); + Require(isspace_nid_postfix(c) == (bits & 0x44) && isblank_nid_postfix(c) == (bits & 0x404) && iscntrl_nid_postfix(c) == (bits & 0x80)); + Require(isprint_nid_postfix(c) == (bits & 0x3e) && isgraph_nid_postfix(c) == (bits & 0x3a) && ispunct_nid_postfix(c) == (bits & 0x08)); + } +} +static void CheckSignedCharValues() { + for (int c = -128; c <= -2; ++c) { + Require(isupper_nid_postfix(c) == 0 && islower_nid_postfix(c) == 0 && isalpha_nid_postfix(c) == 0 && isdigit_nid_postfix(c) == 0); + Require(isalnum_nid_postfix(c) == 0 && isspace_nid_postfix(c) == 0 && isblank_nid_postfix(c) == 0 && iscntrl_nid_postfix(c) == 0); + Require(isprint_nid_postfix(c) == 0 && isgraph_nid_postfix(c) == 0 && ispunct_nid_postfix(c) == 0 && isxdigit_nid_postfix(c) == 0); + Require(toupper_nid_postfix(c) == c && tolower_nid_postfix(c) == c); + } +} + int main() { + CheckConsoleClassValues(); + CheckSignedCharValues(); Require(std::setlocale(LC_CTYPE, "C") != nullptr); for (int category = 0; category <= 6; ++category) { const char* result = setlocale_nid_postfix(category, "C"); diff --git a/core/libs/tests/GuestContentExport.cpp b/core/libs/tests/GuestContentExport.cpp index 07fa418b6d..07036b1d9c 100644 --- a/core/libs/tests/GuestContentExport.cpp +++ b/core/libs/tests/GuestContentExport.cpp @@ -59,10 +59,7 @@ int main() { Require(sceContentExportStart() == static_cast(0x809D3018u)); Require(sceContentExportTerm() == static_cast(0x809D3003u)); int first = *sessions.begin(); - Require(Throws([&] { sceContentExportGetProgress(first); })); Require(sceContentExportCancel(first) == static_cast(0x809D3002u)); - Require(Throws([&] { Export(first); })); - Require(path[0] == 0); Require(sceContentExportFinish(first) == 0); Require(Throws([&] { sceContentExportFinish(first); })); Require(Throws([&] { sceContentExportGetProgress(first); })); diff --git a/core/libs/tests/GuestContentSearch.cpp b/core/libs/tests/GuestContentSearch.cpp index c262e8a9df..bac0286fe0 100644 --- a/core/libs/tests/GuestContentSearch.cpp +++ b/core/libs/tests/GuestContentSearch.cpp @@ -2,6 +2,9 @@ #include "SceTypes.hpp" #include #include +#ifdef __APPLE__ +#include +#endif #include namespace { @@ -33,12 +36,6 @@ constexpr int ErrorInvalidArgument = static_cast(0x809D1003u); constexpr int ErrorAlreadyInitialized = static_cast(0x809D1002u); constexpr int ErrorLimitTooBig = static_cast(0x809D100Au); -struct MetadataValue { - std::int32_t size; - std::int32_t type; - std::int64_t value; -}; - } int main() { @@ -52,9 +49,6 @@ int main() { std::int64_t size = -1; std::int64_t updateId = -1; std::int32_t metadataId = -1; - std::int32_t metadataType = -1; - std::int32_t fieldSize = -1; - MetadataValue value{-1, -1, -1}; Require(sceContentSearchGetContentLastUpdateId(&updateId) == ErrorNotInitialized); Require(sceContentSearchGetNumOfContent(&columnSet, 1, &count, &updateId) == ErrorNotInitialized); @@ -122,13 +116,7 @@ int main() { Require(sceContentSearchGetTotalContentSize(&columnSet, 0, &size, &updateId) == ErrorInvalidArgument); Require(count == -1 && updateId == -1); - Require(Throws([&] { sceContentSearchOpenMetadata("/data/photo.jpg", &metadataId); })); - Require(Throws([&] { sceContentSearchOpenMetadataByContentId(1, &metadataId); })); - Require(metadataId == -1); Require(sceContentSearchCloseMetadata(1) == ErrorInvalidArgument); - Require(Throws([&] { sceContentSearchGetMetadataFieldInfo(1, "title", &metadataType, &fieldSize); })); - Require(Throws([&] { sceContentSearchGetMetadataValue(1, "title", &value); })); - Require(metadataType == -1 && fieldSize == -1 && value.size == -1); Require(sceContentSearchTerm() == 0); Require(sceContentSearchGetContentLastUpdateId(&updateId) == ErrorNotInitialized); diff --git a/core/libs/tests/GuestConvertKeycode.cpp b/core/libs/tests/GuestConvertKeycode.cpp index 8bf7b815b0..d2d3cb463f 100644 --- a/core/libs/tests/GuestConvertKeycode.cpp +++ b/core/libs/tests/GuestConvertKeycode.cpp @@ -15,6 +15,9 @@ static void Require(bool value) { if (!value) std::abort(); } +static_assert(ImeKeyboardType::LookUp(INT32_MIN, 0, 0) == 0); +static_assert(ImeKeyboardType::LookUp(INT32_MAX, 0, 0) == 0); + static void SetLanguage(const char* value) { #ifdef _WIN32 Require(_putenv_s("ANYPS5_LANGUAGE", value != nullptr ? value : "") == 0); diff --git a/core/libs/tests/GuestConvertKeycodeTitle.cpp b/core/libs/tests/GuestConvertKeycodeTitle.cpp index c398ae1ba0..21ccc29fa5 100644 --- a/core/libs/tests/GuestConvertKeycodeTitle.cpp +++ b/core/libs/tests/GuestConvertKeycodeTitle.cpp @@ -18,5 +18,11 @@ int main() { std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); if (!ProcessParametersFixture::Install(process.data(), process.size())) return 2; std::uint32_t type = 0xFFFFFFFFu; + std::uint16_t virtualKey = 0; + char16_t character = 0; + if (sceConvertKeycodeGetVirtualKeycode(0, 0, nullptr) != static_cast(0x80BC0031u)) return 1; + if (sceConvertKeycodeGetVirtualKeycode(0, 0, &virtualKey) != static_cast(0x80BC001Du)) return 1; + if (sceConvertKeycodeGetCharacter(0, 0, 0, nullptr) != static_cast(0x80BC0031u)) return 1; + if (sceConvertKeycodeGetCharacter(0, 0, 0, &character) != static_cast(0x80BC001Du)) return 1; return sceConvertKeycodeGetImeKeyboardType(0x10000000, &type) == 0 && type == 4 ? 0 : 1; } diff --git a/core/libs/tests/GuestCrt.cpp b/core/libs/tests/GuestCrt.cpp index fc18d84627..1e8f1887f5 100644 --- a/core/libs/tests/GuestCrt.cpp +++ b/core/libs/tests/GuestCrt.cpp @@ -173,6 +173,13 @@ int main() { known.tm_year = 9000; text[0] = 'x'; Require(sceAmprAsctimeS(text, sizeof(text), &known) == EINVAL && text[0] == '\0'); + for (const int year : {INT_MAX, INT_MAX - 1899, INT_MIN}) { + known.tm_year = year; + text[0] = 'x'; + Require(sceAmprAsctimeS(text, sizeof(text), &known) == EINVAL && text[0] == '\0'); + text[0] = 'x'; + Require(asctime_s_nid_postfix(text, sizeof(text), &known) == EINVAL && text[0] == '\0'); + } known.tm_year = 70; known.tm_mday = 32; text[0] = 'x'; diff --git a/core/libs/tests/GuestCurrentCpu.cpp b/core/libs/tests/GuestCurrentCpu.cpp index 17cda444cb..323eead874 100644 --- a/core/libs/tests/GuestCurrentCpu.cpp +++ b/core/libs/tests/GuestCurrentCpu.cpp @@ -33,6 +33,7 @@ int main() { const KernelCpumask available = sceKernelGetAvailableCpumask(); Require(sceKernelGetMainSocId() == 0 && sceKernelGetCpumode() == 0); Require(sceKernelGetCpuFrequency() > 0 && static_cast(sceKernelGetCpuFrequency()) == sceKernelGetTscFrequency()); + Require(sceKernelGetTscFrequency() >= 100000000u && sceKernelGetTscFrequency() <= 10000000000u); const std::array affinities{available, 0x1FFB, 0x3, 0x3F, 0x1000, 0x1, 0xFFFF, 0x10000}; for (const KernelCpumask affinity : affinities) { diff --git a/core/libs/tests/GuestDinkumwareAtomics.cpp b/core/libs/tests/GuestDinkumwareAtomics.cpp index 47765a29c8..782953cae3 100644 --- a/core/libs/tests/GuestDinkumwareAtomics.cpp +++ b/core/libs/tests/GuestDinkumwareAtomics.cpp @@ -10,6 +10,50 @@ static void Require(bool value) { if (!value) std::abort(); } constexpr int Relaxed = 0, Acquire = 2, Release = 3, AcqRel = 4, SeqCst = 5; +static void CheckRemainingTyped() { + std::uint8_t byte = 0; + Require(_Atomic_load_1_nid_postfix(&byte, SeqCst) == 0); + Require(_Atomic_exchange_1_nid_postfix(&byte, 0xa5, SeqCst) == 0 && byte == 0xa5); + std::uint8_t expectedByte = 0; + Require(_Atomic_compare_exchange_strong_1_nid_postfix(&byte, &expectedByte, 1, SeqCst, SeqCst) == 0 && expectedByte == 0xa5 && byte == 0xa5); + Require(_Atomic_compare_exchange_strong_1_nid_postfix(&byte, &expectedByte, 0x3c, SeqCst, SeqCst) == 1 && byte == 0x3c); + expectedByte = 0; + Require(_Atomic_compare_exchange_weak_1_nid_postfix(&byte, &expectedByte, 1, SeqCst, Relaxed) == 0 && expectedByte == 0x3c); + Require(_Atomic_compare_exchange_weak_1_nid_postfix(&byte, &expectedByte, 0x0f, SeqCst, Relaxed) == 1 && byte == 0x0f); + Require(_Atomic_fetch_sub_1_nid_postfix(&byte, 0x10, SeqCst) == 0x0f && byte == 0xff); + Require(_Atomic_fetch_and_1_nid_postfix(&byte, 0x3c, Relaxed) == 0xff && byte == 0x3c); + Require(_Atomic_fetch_xor_1_nid_postfix(&byte, 0xff, AcqRel) == 0x3c && byte == 0xc3); + + std::uint16_t half = 0; + _Atomic_store_2_nid_postfix(&half, 0x1234, Release); + Require(half == 0x1234 && _Atomic_exchange_2_nid_postfix(&half, 0xfff0, SeqCst) == 0x1234 && half == 0xfff0); + Require(_Atomic_fetch_add_2_nid_postfix(&half, 0x20, SeqCst) == 0xfff0 && half == 0x0010); + Require(_Atomic_fetch_and_2_nid_postfix(&half, 0x0018, SeqCst) == 0x0010 && half == 0x0010); + Require(_Atomic_fetch_or_2_nid_postfix(&half, 0x0101, SeqCst) == 0x0010 && half == 0x0111); + Require(_Atomic_fetch_xor_2_nid_postfix(&half, 0x0110, SeqCst) == 0x0111 && half == 0x0001); + std::uint16_t expectedHalf = 2; + Require(_Atomic_compare_exchange_weak_2_nid_postfix(&half, &expectedHalf, 9, SeqCst, Relaxed) == 0 && expectedHalf == 1); + Require(_Atomic_compare_exchange_weak_2_nid_postfix(&half, &expectedHalf, 9, SeqCst, Relaxed) == 1 && half == 9); + + std::uint32_t word = 0; + _Atomic_store_4_nid_postfix(&word, 0x00ff00ffu, SeqCst); + Require(word == 0x00ff00ffu && _Atomic_fetch_or_4_nid_postfix(&word, 0xff000000u, SeqCst) == 0x00ff00ffu && word == 0xffff00ffu); + Require(_Atomic_fetch_xor_4_nid_postfix(&word, 0xffffffffu, SeqCst) == 0xffff00ffu && word == 0x0000ff00u); + std::uint32_t expectedWord = 0; + Require(_Atomic_compare_exchange_strong_4_nid_postfix(&word, &expectedWord, 1, SeqCst, SeqCst) == 0 && expectedWord == 0x0000ff00u); + Require(_Atomic_compare_exchange_strong_4_nid_postfix(&word, &expectedWord, 1, SeqCst, SeqCst) == 1 && word == 1); + + std::uint64_t wide = 0x8000000000000001ull; + Require(_Atomic_fetch_sub_8_nid_postfix(&wide, 2, SeqCst) == 0x8000000000000001ull && wide == 0x7fffffffffffffffull); + Require(_Atomic_fetch_and_8_nid_postfix(&wide, 0xffffffff00000000ull, SeqCst) == 0x7fffffffffffffffull && wide == 0x7fffffff00000000ull); + Require(_Atomic_fetch_or_8_nid_postfix(&wide, 0xffull, SeqCst) == 0x7fffffff00000000ull && wide == 0x7fffffff000000ffull); + std::uint64_t expectedWide = 0; + Require(_Atomic_compare_exchange_strong_8_nid_postfix(&wide, &expectedWide, 5, SeqCst, SeqCst) == 0 && expectedWide == 0x7fffffff000000ffull); + Require(_Atomic_compare_exchange_strong_8_nid_postfix(&wide, &expectedWide, 5, SeqCst, SeqCst) == 1 && wide == 5); + + Require(_Atomic_is_lock_free_1_nid_postfix() == 1 && _Atomic_is_lock_free_2_nid_postfix() == 1 && _Atomic_is_lock_free_8_nid_postfix() == 1); +} + static void CheckTyped() { std::uint8_t byte = 0; _Atomic_store_1_nid_postfix(&byte, 7, SeqCst); @@ -130,6 +174,7 @@ static void CheckSized() { } int main() { + CheckRemainingTyped(); CheckTyped(); CheckSized(); CheckFlagsAndFences(); diff --git a/core/libs/tests/GuestDinkumwareFetch.cpp b/core/libs/tests/GuestDinkumwareFetch.cpp new file mode 100644 index 0000000000..81a8d10c1b --- /dev/null +++ b/core/libs/tests/GuestDinkumwareFetch.cpp @@ -0,0 +1,133 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::exit(1); +} + +template +using Fetch = TValue (APS5_VABI*)(volatile TValue*, TValue); + +template +static TValue Expected(TValue before, TValue operand, const std::string& operation) { + if (operation == "and") return before & operand; + if (operation == "or") return before | operand; + return before ^ operand; +} + +template +static void Check(Fetch fetch, const std::string& operation) { + constexpr TValue maximum = std::numeric_limits::max(); + std::array guarded{maximum, 0, maximum}; + const auto one = [&](TValue before, TValue operand) { + guarded[1] = before; + Require(fetch(&guarded[1], operand) == before, "fetch must return the original value"); + Require(guarded[1] == Expected(before, operand, operation), "bitwise result"); + Require(guarded[0] == maximum && guarded[2] == maximum, "neighboring storage changed"); + }; + if constexpr (sizeof(TValue) == 1) { + for (unsigned before = 0; before < 256; ++before) + for (unsigned operand = 0; operand < 256; ++operand) + one(static_cast(before), static_cast(operand)); + } else { + constexpr std::array values{0, 1, maximum, static_cast(maximum >> 1), static_cast(TValue{1} << (sizeof(TValue) * 8 - 1)), static_cast(0xA55AA55A)}; + for (TValue before : values) for (TValue operand : values) one(before, operand); + std::uint32_t random = 0x31415926; + for (unsigned i = 0; i < 4096; ++i) { + random = random * 1664525u + 1013904223u; + const TValue before = static_cast(random); + random = random * 1664525u + 1013904223u; + one(before, static_cast(random)); + } + } + guarded[1] = operation == "and" ? maximum : 0; + std::array workers; + std::barrier start(4); + for (unsigned index = 0; index < workers.size(); ++index) { + workers[index] = std::thread([&, index] { + const TValue bit = static_cast(TValue{1} << index); + const TValue operand = operation == "and" ? static_cast(~bit) : bit; + start.arrive_and_wait(); + for (unsigned iteration = 0; iteration < 1025; ++iteration) { + const TValue previous = fetch(&guarded[1], operand); + const bool expected = operation == "and" ? iteration == 0 : operation == "or" ? iteration != 0 : iteration % 2 != 0; + Require(((previous & bit) != 0) == expected, "concurrent original-bit history"); + } + }); + } + for (auto& worker : workers) worker.join(); + Require(guarded[1] == (operation == "and" ? static_cast(maximum & ~TValue{15}) : TValue{15}), "concurrent updates lost"); + Require(guarded[0] == maximum && guarded[2] == maximum, "concurrent neighboring storage changed"); + std::array flags{}; + std::array observed{}; + std::barrier phase(3); + const auto litmus = [&](unsigned index) { + for (unsigned i = 0; i < 512; ++i) { + phase.arrive_and_wait(); + fetch(&flags[index], operation == "and" ? TValue{0} : TValue{1}); + observed[index] = fetch(&flags[1 - index], operation == "and" ? maximum : TValue{0}); + phase.arrive_and_wait(); + } + }; + std::thread first(litmus, 0), second(litmus, 1); + for (unsigned i = 0; i < 512; ++i) { + flags.fill(operation == "and" ? maximum : TValue{0}); + phase.arrive_and_wait(); + phase.arrive_and_wait(); + const TValue initial = operation == "and" ? maximum : TValue{0}; + Require(observed[0] != initial || observed[1] != initial, "forbidden sequentially consistent outcome"); + } + first.join(); + second.join(); +} + +int main(int argc, char** argv) { + Require(argc == 5, "library directory, patched or unpatched, operation, width"); + const auto path = std::filesystem::absolute(argv[1]) / "libc.prx"; + const std::string operation = argv[3]; + const int width = std::atoi(argv[4]); + Require(operation == "and" || operation == "or" || operation == "xor", "operation"); + Require(width == 1 || width == 2 || width == 4, "width"); + const std::string name = "_Fetch_" + operation + "_seq_cst_" + std::to_string(width); + const std::string symbol = std::string(argv[2]) == "patched" ? Nid::ComputeNid(name, "") : name + "_nid_postfix"; +#ifdef _WIN32 + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + const auto startup = GetModuleHandleW(L"libc.prx"); + const auto library = LoadLibraryExW(path.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); + Require(library != nullptr, "load libc"); + std::array loaded{}; + const auto length = GetModuleFileNameW(library, loaded.data(), static_cast(loaded.size())); + Require(length != 0 && length < loaded.size(), "loaded libc path"); + Require(std::filesystem::equivalent(path, std::filesystem::path(loaded.data())), "wrong libc loaded"); + if (startup) Require(startup == library, "startup libc differs from selected libc"); + std::printf("Selected libc: %ls (startup module %s)\n", loaded.data(), startup ? "same" : "not imported"); + const auto function = reinterpret_cast(GetProcAddress(library, symbol.c_str())); +#else + const auto library = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); + if (!library) std::fprintf(stderr, "%s\n", dlerror()); + Require(library != nullptr, "load libc"); + const auto function = dlsym(library, symbol.c_str()); +#endif + if (!function) std::fprintf(stderr, "Missing %s (%s)\n", name.c_str(), symbol.c_str()); + Require(function != nullptr, "cannot exercise missing helper"); + if (width == 1) Check(reinterpret_cast>(function), operation); + if (width == 2) Check(reinterpret_cast>(function), operation); + if (width == 4) Check(reinterpret_cast>(function), operation); + std::printf("%s: value, original result, bounds, concurrency and ordering checks passed\n", name.c_str()); +} diff --git a/core/libs/tests/GuestDinkumwareInternals.cpp b/core/libs/tests/GuestDinkumwareInternals.cpp index 6e1ffdf7e4..c1be3d457e 100644 --- a/core/libs/tests/GuestDinkumwareInternals.cpp +++ b/core/libs/tests/GuestDinkumwareInternals.cpp @@ -1,16 +1,25 @@ #include "prx/libc/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include +#include #include #include #include #include #include #include +#include namespace { constexpr int RangeError = 34; constexpr int InvalidArgument = 22; +constexpr short Denormal = -2; +constexpr short Finite = -1; +constexpr short Zero = 0; +constexpr short Infinite = 1; +constexpr short NotANumber = 2; +constexpr int FloatingExceptions = FE_INVALID | FE_DIVBYZERO | FE_OVERFLOW | FE_UNDERFLOW | FE_INEXACT; void Require(bool condition, const char* message) { if (!condition) { @@ -27,6 +36,24 @@ void CheckStod(const char* text, std::int64_t pten, double expected, long consum Require(same && end - text == consumed && *__error_nid_postfix() == error, message); } +void CheckExp(double x, double y, short offset, double expected, short expectedKind, int expectedErrno, int expectedFlags, const char* message) { + errno = 0; + std::feclearexcept(FE_ALL_EXCEPT); + double result = x; + const short kind = _Exp_nid_postfix(&result, y, offset); + const bool same = std::isnan(expected) ? std::isnan(result) : result == expected && std::signbit(result) == std::signbit(expected); + Require(kind == expectedKind && same && errno == expectedErrno && (std::fetestexcept(FE_ALL_EXCEPT) & FloatingExceptions) == expectedFlags, message); +} + +void CheckFExp(float x, float y, short offset, float expected, short expectedKind, int expectedErrno, int expectedFlags, const char* message) { + errno = 0; + std::feclearexcept(FE_ALL_EXCEPT); + float result = x; + const short kind = _FExp_nid_postfix(&result, y, offset); + const bool same = std::isnan(expected) ? std::isnan(result) : result == expected && std::signbit(result) == std::signbit(expected); + Require(kind == expectedKind && same && errno == expectedErrno && (std::fetestexcept(FE_ALL_EXCEPT) & FloatingExceptions) == expectedFlags, message); +} + void TestStod() { CheckStod("1.5", 0, 1.5, 3, 0, "unscaled decimal"); CheckStod("1.5", 1, 15.0, 3, 0, "decimal scaled up"); @@ -63,6 +90,30 @@ void TestStoll() { Require(_Stoll_nid_postfix("9223372036854775808", &end, 10) == 9223372036854775807ll && *__error_nid_postfix() == RangeError, "_Stoll saturates"); } +void TestArithmetic() { + Require(_Divide_nid_postfix(3.0, 2.0) == 1.5, "_Divide quotient"); + Require(std::isinf(_Divide_nid_postfix(1.0, 0.0)) && !std::signbit(_Divide_nid_postfix(1.0, 0.0)), "_Divide positive infinity"); + Require(std::isinf(_Divide_nid_postfix(-1.0, 0.0)) && std::signbit(_Divide_nid_postfix(-1.0, 0.0)), "_Divide negative infinity"); + Require(std::isnan(_Divide_nid_postfix(0.0, 0.0)), "_Divide invalid"); + Require(_Divide_nid_postfix(-0.0, 2.0) == 0.0 && std::signbit(_Divide_nid_postfix(-0.0, 2.0)), "_Divide signed zero"); + Require(_Divide_nid_postfix(1.0, INFINITY) == 0.0 && !std::signbit(_Divide_nid_postfix(1.0, INFINITY)), "_Divide finite by infinity"); + Require(std::isnan(_Divide_nid_postfix(INFINITY, INFINITY)), "_Divide infinity by infinity"); + Require(_Recip_nid_postfix(4.0) == 0.25, "_Recip quotient"); + Require(_Recip_nid_postfix(-4.0) == -0.25, "_Recip negative quotient"); + Require(std::isinf(_Recip_nid_postfix(-0.0)) && std::signbit(_Recip_nid_postfix(-0.0)), "_Recip signed infinity"); + Require(_Recip_nid_postfix(INFINITY) == 0.0 && !std::signbit(_Recip_nid_postfix(INFINITY)), "_Recip infinity"); + Require(std::isnan(_Recip_nid_postfix(NAN)), "_Recip NaN"); + Require(_FDivide_nid_postfix(3.0f, 2.0f) == 1.5f, "_FDivide quotient"); + Require(std::isinf(_FDivide_nid_postfix(-1.0f, 0.0f)) && std::signbit(_FDivide_nid_postfix(-1.0f, 0.0f)), "_FDivide negative infinity"); + Require(std::isnan(_FDivide_nid_postfix(0.0f, 0.0f)), "_FDivide invalid"); + Require(_FDivide_nid_postfix(-0.0f, 2.0f) == 0.0f && std::signbit(_FDivide_nid_postfix(-0.0f, 2.0f)), "_FDivide signed zero"); + Require(_FRecip_nid_postfix(4.0f) == 0.25f, "_FRecip quotient"); + Require(_FRecip_nid_postfix(-4.0f) == -0.25f, "_FRecip negative quotient"); + Require(std::isinf(_FRecip_nid_postfix(-0.0f)) && std::signbit(_FRecip_nid_postfix(-0.0f)), "_FRecip signed infinity"); + Require(_FRecip_nid_postfix(INFINITY) == 0.0f && !std::signbit(_FRecip_nid_postfix(INFINITY)), "_FRecip infinity"); + Require(std::isnan(_FRecip_nid_postfix(NAN)), "_FRecip NaN"); +} + void TestFloatErrors() { char* end = nullptr; int error = 99; @@ -158,6 +209,182 @@ void TestIntegerErrors() { Require(_Stoullx_nid_postfix("18446744073709551616", &end, 10, &error) == 18446744073709551615ull && error == RangeError && *__error_nid_postfix() == RangeError, "_Stoullx overflow"); } +void TestDoubleHelpers() { + const double e = 0x1.5bf0a8b145769p+1; + CheckExp(1.0, 1.0, 0, e, Finite, 0, 0, "_Exp e"); + CheckExp(0.0, 1.0, 0, 1.0, Finite, 0, 0, "_Exp zero exponent"); + CheckExp(0.0, 5.0, 0, 5.0, Finite, 0, 0, "_Exp multiplier at zero exponent"); + CheckExp(1.0, -0.0, 0, -0.0, Zero, 0, 0, "_Exp keeps a negative zero multiplier"); + CheckExp(1.0, 0.0, 0, 0.0, Zero, 0, 0, "_Exp zero multiplier"); + CheckExp(-1.0, -0.0, 0, -0.0, Zero, 0, 0, "_Exp negative zero multiplier"); + CheckExp(INFINITY, -0.0, 0, std::numeric_limits::quiet_NaN(), NotANumber, EDOM, FE_INVALID, "_Exp positive infinity with zero multiplier is invalid"); + CheckExp(INFINITY, 0.0, 0, std::numeric_limits::quiet_NaN(), NotANumber, EDOM, FE_INVALID, "_Exp positive infinity with positive zero is invalid"); + CheckExp(INFINITY, 1.0, 0, INFINITY, Infinite, 0, 0, "_Exp positive infinity"); + CheckExp(INFINITY, -1.0, 0, -INFINITY, Infinite, 0, 0, "_Exp positive infinity preserves the multiplier sign"); + CheckExp(INFINITY, INFINITY, 0, INFINITY, Infinite, 0, 0, "_Exp positive infinity with infinite multiplier"); + CheckExp(INFINITY, -INFINITY, 0, -INFINITY, Infinite, 0, 0, "_Exp positive infinity with negative infinite multiplier"); + CheckExp(-INFINITY, 1.0, 0, 0.0, Zero, 0, 0, "_Exp negative infinity"); + CheckExp(-INFINITY, -1.0, 0, -0.0, Zero, 0, 0, "_Exp negative infinity preserves the multiplier sign"); + CheckExp(-INFINITY, 0.0, 0, 0.0, Zero, 0, 0, "_Exp negative infinity with positive zero"); + CheckExp(-INFINITY, -0.0, 0, -0.0, Zero, 0, 0, "_Exp negative infinity with negative zero"); + CheckExp(-INFINITY, INFINITY, 0, std::numeric_limits::quiet_NaN(), NotANumber, EDOM, FE_INVALID, "_Exp negative infinity with infinite multiplier is invalid"); + CheckExp(-INFINITY, -INFINITY, 0, std::numeric_limits::quiet_NaN(), NotANumber, EDOM, FE_INVALID, "_Exp negative infinity with negative infinite multiplier is invalid"); + CheckExp(0.0, INFINITY, 0, INFINITY, Infinite, 0, 0, "_Exp zero exponent with infinite multiplier"); + CheckExp(0.0, -INFINITY, 0, -INFINITY, Infinite, 0, 0, "_Exp zero exponent with negative infinite multiplier"); + CheckExp(1.0, INFINITY, 0, INFINITY, Infinite, 0, 0, "_Exp finite exponent with infinite multiplier"); + CheckExp(1.0, -INFINITY, 0, -INFINITY, Infinite, 0, 0, "_Exp finite exponent with negative infinite multiplier"); + CheckExp(1.0, std::numeric_limits::quiet_NaN(), 0, 1.0, NotANumber, 0, 0, "_Exp NaN multiplier leaves the exponent"); + CheckExp(INFINITY, std::numeric_limits::quiet_NaN(), 0, INFINITY, NotANumber, 0, 0, "_Exp NaN multiplier leaves an infinite exponent"); + CheckExp(std::numeric_limits::quiet_NaN(), 0.0, 0, std::numeric_limits::quiet_NaN(), NotANumber, 0, 0, "_Exp NaN exponent with zero multiplier"); + CheckExp(std::numeric_limits::quiet_NaN(), 1.0, 0, std::numeric_limits::quiet_NaN(), NotANumber, 0, 0, "_Exp NaN exponent"); + CheckExp(std::numeric_limits::quiet_NaN(), INFINITY, 0, std::numeric_limits::quiet_NaN(), NotANumber, 0, 0, "_Exp NaN exponent with infinite multiplier"); + CheckExp(1843.0, 1.0, 0, INFINITY, Infinite, ERANGE, FE_OVERFLOW | FE_INEXACT, "_Exp positive cutoff"); + CheckExp(1843.0, -1.0, 0, -INFINITY, Infinite, ERANGE, FE_OVERFLOW | FE_INEXACT, "_Exp positive cutoff preserves the multiplier sign"); + CheckExp(1843.0, 1.0, -2660, INFINITY, Infinite, ERANGE, FE_OVERFLOW | FE_INEXACT, "_Exp positive cutoff ignores a compensating offset"); + CheckExp(-1843.0, 1.0, 0, 0.0, Zero, 0, 0, "_Exp negative cutoff"); + CheckExp(-1843.0, -1.0, 0, -0.0, Zero, 0, 0, "_Exp negative cutoff preserves the multiplier sign"); + CheckExp(-1843.0, 1.0, 2660, 0.0, Zero, 0, 0, "_Exp negative cutoff ignores a compensating offset"); + CheckExp(1843.0, 0.0, 0, 0.0, Zero, 0, 0, "_Exp positive cutoff with zero multiplier"); + CheckExp(1843.0, std::numeric_limits::quiet_NaN(), 0, 1843.0, NotANumber, 0, 0, "_Exp positive cutoff with NaN multiplier"); + CheckExp(0.0, 1.0, -1074, std::numeric_limits::denorm_min(), Finite, 0, 0, "_Exp exact minimum subnormal"); + CheckExp(0.0, 1.0, -1075, 0.0, Zero, ERANGE, FE_UNDERFLOW | FE_INEXACT, "_Exp scaled underflow"); + CheckExp(0.0, -1.0, -1075, -0.0, Zero, ERANGE, FE_UNDERFLOW | FE_INEXACT, "_Exp scaled underflow preserves the multiplier sign"); + CheckExp(0.0, 1.0, 100, std::scalbn(1.0, 100), Finite, 0, 0, "_Exp scaled finite result"); + CheckExp(0.0, 1.0, 2000, INFINITY, Infinite, ERANGE, FE_OVERFLOW | FE_INEXACT, "_Exp scaled overflow"); + CheckExp(1842.0, 1.0, 0, INFINITY, Infinite, ERANGE, FE_OVERFLOW | FE_INEXACT, "_Exp inside the positive cutoff overflows"); + CheckExp(-1842.0, 1.0, 0, 0.0, Zero, ERANGE, FE_UNDERFLOW | FE_INEXACT, "_Exp inside the negative cutoff underflows"); + + errno = 0; + std::feclearexcept(FE_ALL_EXCEPT); + double result = 1.0; + Require(_Exp_nid_postfix(&result, 3.0, 0) == Finite && std::fabs(result - 3.0 * e) <= 3.0 * e * 1e-14, "_Exp multiplier"); + result = 1.0; + Require(_Exp_nid_postfix(&result, -2.0, 0) == Finite && std::fabs(result + 2.0 * e) <= 2.0 * e * 1e-14, "_Exp negative multiplier"); + result = 1.0; + Require(_Exp_nid_postfix(&result, 1.0, 10) == Finite && std::fabs(result - e * 1024.0) <= e * 1024.0 * 1e-14, "_Exp scale"); + for (int i = -200; i <= 200; ++i) { + const double x = static_cast(i) / 16.0; + result = x; + Require(_Exp_nid_postfix(&result, 1.0, 0) == Finite && std::fabs(result - std::exp(x)) <= std::fabs(std::exp(x)) * 1e-13, "_Exp finite sweep"); + } + + errno = 0; + std::feclearexcept(FE_ALL_EXCEPT); + result = 1.0; + Require(_Exp_nid_postfix(&result, std::numeric_limits::denorm_min(), 0) == Finite && std::fpclassify(result) == FP_SUBNORMAL && errno == 0 && std::fetestexcept(FE_ALL_EXCEPT) == 0, "_Exp subnormal result classification"); + errno = 0; + std::feclearexcept(FE_ALL_EXCEPT); + result = -740.0; + Require(_Exp_nid_postfix(&result, 1.0, 0) == Finite && std::fpclassify(result) == FP_SUBNORMAL && errno == 0 && std::fetestexcept(FE_ALL_EXCEPT) == 0, "_Exp negative exponent subnormal result"); + errno = 0; + std::feclearexcept(FE_ALL_EXCEPT); + result = 1.0; + Require(_Exp_nid_postfix(&result, std::numeric_limits::max(), 0) == Infinite && std::isinf(result) && errno == ERANGE && (std::fetestexcept(FE_ALL_EXCEPT) & (FE_OVERFLOW | FE_INEXACT)) == (FE_OVERFLOW | FE_INEXACT), "_Exp finite multiplier overflow"); + + const double largeExpected = std::scalbn(std::exp(710.0 - 1024.0 * std::log(2.0)) * 1e-100, 1024); + errno = 0; + std::feclearexcept(FE_ALL_EXCEPT); + result = 710.0; + Require(_Exp_nid_postfix(&result, 1e-100, 0) == Finite && std::isfinite(result) && std::fabs(result - largeExpected) <= largeExpected * 1e-13 && errno == 0 && (std::fetestexcept(FE_ALL_EXCEPT) & (FE_OVERFLOW | FE_UNDERFLOW)) == 0, "_Exp large exponent stays finite"); + const double smallExpected = std::scalbn(std::exp(-710.0 + 1024.0 * std::log(2.0)) * 1e300, -1024); + errno = 0; + std::feclearexcept(FE_ALL_EXCEPT); + result = -710.0; + Require(_Exp_nid_postfix(&result, 1e300, 0) == Finite && std::isfinite(result) && std::fabs(result - smallExpected) <= smallExpected * 1e-13 && errno == 0 && (std::fetestexcept(FE_ALL_EXCEPT) & (FE_OVERFLOW | FE_UNDERFLOW)) == 0, "_Exp small exponent stays finite"); + const double multiplierExpected = std::exp(-0.5) * std::numeric_limits::max(); + errno = 0; + std::feclearexcept(FE_ALL_EXCEPT); + result = -0.5; + Require(_Exp_nid_postfix(&result, std::numeric_limits::max(), 0) == Finite && std::isfinite(result) && std::fabs(result - multiplierExpected) <= multiplierExpected * 1e-13 && errno == 0 && (std::fetestexcept(FE_ALL_EXCEPT) & (FE_OVERFLOW | FE_UNDERFLOW)) == 0, "_Exp large multiplier stays finite"); + + const float ef = 0x1.5bf0a8p+1f; + CheckFExp(1.0f, 1.0f, 0, ef, Finite, 0, 0, "_FExp e"); + CheckFExp(0.0f, 1.0f, 0, 1.0f, Finite, 0, 0, "_FExp zero exponent"); + CheckFExp(0.0f, 5.0f, 0, 5.0f, Finite, 0, 0, "_FExp multiplier at zero exponent"); + CheckFExp(1.0f, -0.0f, 0, -0.0f, Zero, 0, 0, "_FExp keeps a negative zero multiplier"); + CheckFExp(1.0f, 0.0f, 0, 0.0f, Zero, 0, 0, "_FExp zero multiplier"); + CheckFExp(-1.0f, -0.0f, 0, -0.0f, Zero, 0, 0, "_FExp negative zero multiplier"); + CheckFExp(INFINITY, -0.0f, 0, std::numeric_limits::quiet_NaN(), NotANumber, EDOM, FE_INVALID, "_FExp positive infinity with zero multiplier is invalid"); + CheckFExp(INFINITY, 0.0f, 0, std::numeric_limits::quiet_NaN(), NotANumber, EDOM, FE_INVALID, "_FExp positive infinity with positive zero is invalid"); + CheckFExp(INFINITY, 1.0f, 0, INFINITY, Infinite, 0, 0, "_FExp positive infinity"); + CheckFExp(INFINITY, -1.0f, 0, -INFINITY, Infinite, 0, 0, "_FExp positive infinity preserves the multiplier sign"); + CheckFExp(INFINITY, INFINITY, 0, INFINITY, Infinite, 0, 0, "_FExp positive infinity with infinite multiplier"); + CheckFExp(INFINITY, -INFINITY, 0, -INFINITY, Infinite, 0, 0, "_FExp positive infinity with negative infinite multiplier"); + CheckFExp(-INFINITY, 1.0f, 0, 0.0f, Zero, 0, 0, "_FExp negative infinity"); + CheckFExp(-INFINITY, -1.0f, 0, -0.0f, Zero, 0, 0, "_FExp negative infinity preserves the multiplier sign"); + CheckFExp(-INFINITY, 0.0f, 0, 0.0f, Zero, 0, 0, "_FExp negative infinity with positive zero"); + CheckFExp(-INFINITY, -0.0f, 0, -0.0f, Zero, 0, 0, "_FExp negative infinity with negative zero"); + CheckFExp(-INFINITY, INFINITY, 0, std::numeric_limits::quiet_NaN(), NotANumber, EDOM, FE_INVALID, "_FExp negative infinity with infinite multiplier is invalid"); + CheckFExp(-INFINITY, -INFINITY, 0, std::numeric_limits::quiet_NaN(), NotANumber, EDOM, FE_INVALID, "_FExp negative infinity with negative infinite multiplier is invalid"); + CheckFExp(0.0f, INFINITY, 0, INFINITY, Infinite, 0, 0, "_FExp zero exponent with infinite multiplier"); + CheckFExp(0.0f, -INFINITY, 0, -INFINITY, Infinite, 0, 0, "_FExp zero exponent with negative infinite multiplier"); + CheckFExp(1.0f, INFINITY, 0, INFINITY, Infinite, 0, 0, "_FExp finite exponent with infinite multiplier"); + CheckFExp(1.0f, -INFINITY, 0, -INFINITY, Infinite, 0, 0, "_FExp finite exponent with negative infinite multiplier"); + CheckFExp(1.0f, std::numeric_limits::quiet_NaN(), 0, 1.0f, NotANumber, 0, 0, "_FExp NaN multiplier leaves the exponent"); + CheckFExp(INFINITY, std::numeric_limits::quiet_NaN(), 0, INFINITY, NotANumber, 0, 0, "_FExp NaN multiplier leaves an infinite exponent"); + CheckFExp(std::numeric_limits::quiet_NaN(), 0.0f, 0, std::numeric_limits::quiet_NaN(), NotANumber, 0, 0, "_FExp NaN exponent with zero multiplier"); + CheckFExp(std::numeric_limits::quiet_NaN(), 1.0f, 0, std::numeric_limits::quiet_NaN(), NotANumber, 0, 0, "_FExp NaN exponent"); + CheckFExp(std::numeric_limits::quiet_NaN(), INFINITY, 0, std::numeric_limits::quiet_NaN(), NotANumber, 0, 0, "_FExp NaN exponent with infinite multiplier"); + CheckFExp(230.0f, 1.0f, 0, INFINITY, Infinite, ERANGE, FE_OVERFLOW | FE_INEXACT, "_FExp positive cutoff"); + CheckFExp(230.0f, -1.0f, 0, -INFINITY, Infinite, ERANGE, FE_OVERFLOW | FE_INEXACT, "_FExp positive cutoff preserves the multiplier sign"); + CheckFExp(230.0f, 1.0f, -332, INFINITY, Infinite, ERANGE, FE_OVERFLOW | FE_INEXACT, "_FExp positive cutoff ignores the offset"); + CheckFExp(-230.0f, 1.0f, 0, 0.0f, Zero, 0, 0, "_FExp negative cutoff"); + CheckFExp(-230.0f, -1.0f, 0, -0.0f, Zero, 0, 0, "_FExp negative cutoff preserves the multiplier sign"); + CheckFExp(-230.0f, 1.0f, 332, 0.0f, Zero, 0, 0, "_FExp negative cutoff ignores the offset"); + CheckFExp(230.0f, 0.0f, 0, 0.0f, Zero, 0, 0, "_FExp positive cutoff with zero multiplier"); + CheckFExp(230.0f, std::numeric_limits::quiet_NaN(), 0, 230.0f, NotANumber, 0, 0, "_FExp positive cutoff with NaN multiplier"); + CheckFExp(0.0f, 1.0f, -149, std::numeric_limits::denorm_min(), Finite, 0, 0, "_FExp exact minimum subnormal"); + CheckFExp(0.0f, 1.0f, -150, 0.0f, Zero, ERANGE, FE_UNDERFLOW | FE_INEXACT, "_FExp scaled underflow"); + CheckFExp(0.0f, -1.0f, -150, -0.0f, Zero, ERANGE, FE_UNDERFLOW | FE_INEXACT, "_FExp scaled underflow preserves the multiplier sign"); + CheckFExp(0.0f, 1.0f, 100, std::scalbn(1.0f, 100), Finite, 0, 0, "_FExp scaled finite result"); + CheckFExp(0.0f, 1.0f, 200, INFINITY, Infinite, ERANGE, FE_OVERFLOW | FE_INEXACT, "_FExp scaled overflow"); + CheckFExp(229.0f, 1.0f, 0, INFINITY, Infinite, ERANGE, FE_OVERFLOW | FE_INEXACT, "_FExp inside the positive cutoff overflows"); + CheckFExp(-229.0f, 1.0f, 0, 0.0f, Zero, ERANGE, FE_UNDERFLOW | FE_INEXACT, "_FExp inside the negative cutoff underflows"); + + errno = 0; + std::feclearexcept(FE_ALL_EXCEPT); + float resultf = 1.0f; + Require(_FExp_nid_postfix(&resultf, 3.0f, 0) == Finite && std::fabs(resultf - 3.0f * ef) <= 3.0f * ef * 1e-6f, "_FExp multiplier"); + resultf = 1.0f; + Require(_FExp_nid_postfix(&resultf, -2.0f, 0) == Finite && std::fabs(resultf + 2.0f * ef) <= 2.0f * ef * 1e-6f, "_FExp negative multiplier"); + resultf = 1.0f; + Require(_FExp_nid_postfix(&resultf, 1.0f, 10) == Finite && std::fabs(resultf - ef * 1024.0f) <= ef * 1024.0f * 1e-6f, "_FExp scale"); + for (int i = -120; i <= 120; ++i) { + const float x = static_cast(i) / 16.0f; + resultf = x; + Require(_FExp_nid_postfix(&resultf, 1.0f, 0) == Finite && std::fabs(resultf - std::exp(x)) <= std::fabs(std::exp(x)) * 1e-6f, "_FExp finite sweep"); + } + + errno = 0; + std::feclearexcept(FE_ALL_EXCEPT); + resultf = 1.0f; + Require(_FExp_nid_postfix(&resultf, std::numeric_limits::denorm_min(), 0) == Finite && std::fpclassify(resultf) == FP_SUBNORMAL && errno == 0 && std::fetestexcept(FE_ALL_EXCEPT) == 0, "_FExp subnormal result classification"); + errno = 0; + std::feclearexcept(FE_ALL_EXCEPT); + resultf = -100.0f; + Require(_FExp_nid_postfix(&resultf, 1.0f, 0) == Finite && std::fpclassify(resultf) == FP_SUBNORMAL && errno == 0 && std::fetestexcept(FE_ALL_EXCEPT) == 0, "_FExp negative exponent subnormal result"); + errno = 0; + std::feclearexcept(FE_ALL_EXCEPT); + resultf = 1.0f; + Require(_FExp_nid_postfix(&resultf, std::numeric_limits::max(), 0) == Infinite && std::isinf(resultf) && errno == ERANGE && (std::fetestexcept(FE_ALL_EXCEPT) & (FE_OVERFLOW | FE_INEXACT)) == (FE_OVERFLOW | FE_INEXACT), "_FExp finite multiplier overflow"); + + const float largeExpectedf = static_cast(std::exp(100.0) * 1e-35); + errno = 0; + std::feclearexcept(FE_ALL_EXCEPT); + resultf = 100.0f; + Require(_FExp_nid_postfix(&resultf, 1e-35f, 0) == Finite && std::isfinite(resultf) && std::fabs(resultf - largeExpectedf) <= largeExpectedf * 1e-6f && errno == 0 && (std::fetestexcept(FE_ALL_EXCEPT) & (FE_OVERFLOW | FE_UNDERFLOW)) == 0, "_FExp large exponent stays finite"); + const float smallExpectedf = static_cast(std::exp(-100.0) * 1e30); + errno = 0; + std::feclearexcept(FE_ALL_EXCEPT); + resultf = -100.0f; + Require(_FExp_nid_postfix(&resultf, 1e30f, 0) == Finite && std::isfinite(resultf) && std::fabs(resultf - smallExpectedf) <= smallExpectedf * 1e-6f && errno == 0 && (std::fetestexcept(FE_ALL_EXCEPT) & (FE_OVERFLOW | FE_UNDERFLOW)) == 0, "_FExp small exponent stays finite"); + const float multiplierExpectedf = static_cast(std::exp(-0.5) * std::numeric_limits::max()); + errno = 0; + std::feclearexcept(FE_ALL_EXCEPT); + resultf = -0.5f; + Require(_FExp_nid_postfix(&resultf, std::numeric_limits::max(), 0) == Finite && std::isfinite(resultf) && std::fabs(resultf - multiplierExpectedf) <= multiplierExpectedf * 1e-6f && errno == 0 && (std::fetestexcept(FE_ALL_EXCEPT) & (FE_OVERFLOW | FE_UNDERFLOW)) == 0, "_FExp large multiplier stays finite"); +} + void TestDispatch() { Require(_Sin_nid_postfix(0.5, 0) == std::sin(0.5) && _Sin_nid_postfix(0.5, 1) == std::cos(0.5) && _Sin_nid_postfix(0.5, 7) == std::cos(0.5), "_Sin"); Require(_FSin_nid_postfix(0.5f, 0) == std::sin(0.5f) && _FSin_nid_postfix(0.5f, 2) == std::cos(0.5f), "_FSin"); @@ -166,12 +393,26 @@ void TestDispatch() { Require(_FLog_nid_postfix(8.0f, 0) == std::log(8.0f) && _FLog_nid_postfix(100.0f, 1) == 2.0f && _FLog_nid_postfix(8.0f, -3) == 3.0f, "_FLog"); } +void TestLongDoubleHelpers() { + Require(_LDivide_nid_postfix(3.0L, 2.0L) == 1.5L, "_LDivide quotient"); + Require(std::isinf(_LDivide_nid_postfix(1.0L, 0.0L)), "_LDivide infinity"); + Require(std::isnan(_LDivide_nid_postfix(0.0L, 0.0L)), "_LDivide invalid"); + Require(_LRecip_nid_postfix(4.0L) == 0.25L, "_LRecip quotient"); + Require(std::isinf(_LRecip_nid_postfix(-0.0L)) && std::signbit(_LRecip_nid_postfix(-0.0L)), "_LRecip signed infinity"); + Require(_LLog_nid_postfix(8.0L, 0) == std::log(8.0L), "_LLog natural"); + Require(std::fabs(_LLog_nid_postfix(1000.0L, 1) - 3.0L) <= 1e-17L && std::fabs(_LLog_nid_postfix(1000.0L, 5) - 3.0L) <= 1e-17L, "_LLog base 10"); + Require(_LLog_nid_postfix(8.0L, -1) == 3.0L && _LLog_nid_postfix(8.0L, -3) == 3.0L, "_LLog base 2"); +} + } int main() { TestStod(); TestStoll(); + TestDoubleHelpers(); TestFloatErrors(); TestIntegerErrors(); + TestArithmetic(); TestDispatch(); + TestLongDoubleHelpers(); } diff --git a/core/libs/tests/GuestDirectory.cpp b/core/libs/tests/GuestDirectory.cpp index f5646097dc..5f6096246e 100644 --- a/core/libs/tests/GuestDirectory.cpp +++ b/core/libs/tests/GuestDirectory.cpp @@ -173,8 +173,20 @@ int main() { Require(closedir_nid_postfix(directory) == 0); Require(opendir_nid_postfix((root / "missing").string().c_str()) == nullptr); Require(*__error_nid_postfix() == 2); -#ifndef _WIN32 const auto symlinkLoop = root / "symlink-loop"; +#ifdef _WIN32 + if (links) { + Require(CreateSymbolicLinkW(symlinkLoop.c_str(), L"symlink-loop", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + Require(opendir_nid_postfix(symlinkLoop.string().c_str()) == nullptr); + Require(*__error_nid_postfix() == 62); + Require(DeleteFileW(symlinkLoop.c_str()) != 0); + const auto directoryLoop = root / "directory-loop"; + Require(CreateSymbolicLinkW(directoryLoop.c_str(), L"directory-loop", SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + Require(opendir_nid_postfix(directoryLoop.string().c_str()) == nullptr); + Require(*__error_nid_postfix() == 62); + Require(RemoveDirectoryW(directoryLoop.c_str()) != 0); + } +#else std::filesystem::create_symlink("symlink-loop", symlinkLoop); Require(opendir_nid_postfix(symlinkLoop.string().c_str()) == nullptr); Require(*__error_nid_postfix() == 62); diff --git a/core/libs/tests/GuestDiscId.cpp b/core/libs/tests/GuestDiscId.cpp new file mode 100644 index 0000000000..2b77023662 --- /dev/null +++ b/core/libs/tests/GuestDiscId.cpp @@ -0,0 +1,43 @@ +#include "prx/libSceDiscId/include/Api.hpp" +#include +#include +#include +#include + +namespace { + +void Require(bool condition) { + if (!condition) std::abort(); +} + +template +bool Throws(TCall call) { + try { + static_cast(call()); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +} + +int main() { + Require(Throws([] { return sceDiscIdGet(nullptr); })); + Require(Throws([] { return sceDiscIdGet2(nullptr, 0); })); + Require(Throws([] { return sceDiscIdGet2(nullptr, 0xFFFFFFFFu); })); + + std::uint8_t buffer[64]; + std::memset(buffer, 0xAB, sizeof(buffer)); + auto* id = reinterpret_cast(buffer); + auto* id2 = reinterpret_cast(buffer); + + Require(Throws([id] { return sceDiscIdGet(id); })); + Require(Throws([id2] { return sceDiscIdGet2(id2, 0); })); + Require(Throws([id2] { return sceDiscIdGet2(id2, 1); })); + Require(Throws([id2] { return sceDiscIdGet2(id2, 3); })); + Require(Throws([id2] { return sceDiscIdGet2(id2, 0xFFFFFFFFu); })); + for (std::uint8_t byte : buffer) Require(byte == 0xAB); + + return 0; +} diff --git a/core/libs/tests/GuestDup.cpp b/core/libs/tests/GuestDup.cpp index cb2ce4f36e..03db1dcba6 100644 --- a/core/libs/tests/GuestDup.cpp +++ b/core/libs/tests/GuestDup.cpp @@ -1,6 +1,7 @@ #include "prx/libkernel/include/Api.hpp" #include "prx/libc/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -187,6 +188,8 @@ static void DuplicatesShareStatusFlags() { Require(pipe_nid_postfix(pipe) == 0); Require(pipe_nid_postfix(other) == 0); Require(fcntl_nid_postfix(pipe[0], SetDescriptorFlags, 1) == 0); + *__error_nid_postfix() = 0; + Require(fcntl_nid_postfix(pipe[0], 0, INT_MAX) == -1 && *__error_nid_postfix() == 22); const int duplicate = dup_nid_postfix(pipe[0]); Require(duplicate >= 0); Require(fcntl_nid_postfix(duplicate, GetDescriptorFlags) == 0); diff --git a/core/libs/tests/GuestDynamicLoader.cpp b/core/libs/tests/GuestDynamicLoader.cpp index b3e9d0b4e9..57bea0de5b 100644 --- a/core/libs/tests/GuestDynamicLoader.cpp +++ b/core/libs/tests/GuestDynamicLoader.cpp @@ -211,6 +211,54 @@ int main(int argc, char** argv) { std::error_code notEmpty; if (createdLibs) std::filesystem::remove("libs", notEmpty); + const std::filesystem::path nestedRoot = "anyps5-relinked-prx-dir-for-test"; + const std::filesystem::path nestedGuest = nestedRoot / "Engine.Render.prx"; + const std::filesystem::path nestedRelinked = nestedRoot / "prx" / "Engine.Render.prx.guest.prx"; + std::filesystem::create_directories(nestedRelinked.parent_path()); + { + std::ofstream elf(nestedGuest, std::ios::binary); + elf.write("\x7f" "ELF", 4); + } + std::filesystem::copy_file(argv[1], nestedRelinked, std::filesystem::copy_options::overwrite_existing); + void* nested = dlopen_nid_postfix(nestedGuest.string().c_str(), 2); + Require(nested != nullptr); + add = reinterpret_cast(dlsym_nid_postfix(nested, "GuestModuleAdd")); + Require(add && add(20, 22) == 42); + Require(dlclose_nid_postfix(nested) == 0); + std::filesystem::remove_all(nestedRoot); + + const std::filesystem::path layeredRoot = "anyps5-relinked-prx-layers-for-test"; + const std::filesystem::path layeredGuest = layeredRoot / "Engine.Layers.prx"; + std::filesystem::create_directories(layeredRoot / "prx"); + { + std::ofstream elf(layeredGuest, std::ios::binary); + elf.write("" "ELF", 4); + std::ofstream broken(layeredRoot / "prx" / "Engine.Layers.prx.guest.prx", std::ios::binary); + broken << "not a module"; + } + std::filesystem::copy_file(argv[1], layeredRoot / "ENGINE.LAYERS.PRX.GUEST.PRX", std::filesystem::copy_options::overwrite_existing); + void* layered = dlopen_nid_postfix(layeredGuest.string().c_str(), 2); + Require(layered != nullptr); + add = reinterpret_cast(dlsym_nid_postfix(layered, "GuestModuleAdd")); + Require(add && add(19, 23) == 42); + Require(dlclose_nid_postfix(layered) == 0); + std::filesystem::remove_all(layeredRoot); + + const std::filesystem::path foldedRoot = "anyps5-relinked-prx-folded-for-test"; + const std::filesystem::path foldedGuest = foldedRoot / "Engine.Folded.prx"; + std::filesystem::create_directories(foldedRoot / "prx"); + { + std::ofstream elf(foldedGuest, std::ios::binary); + elf.write("" "ELF", 4); + } + std::filesystem::copy_file(argv[1], foldedRoot / "prx" / "ENGINE.FOLDED.PRX.GUEST.PRX", std::filesystem::copy_options::overwrite_existing); + void* folded = dlopen_nid_postfix(foldedGuest.string().c_str(), 2); + Require(folded != nullptr); + add = reinterpret_cast(dlsym_nid_postfix(folded, "GuestModuleAdd")); + Require(add && add(10, 32) == 42); + Require(dlclose_nid_postfix(folded) == 0); + std::filesystem::remove_all(foldedRoot); + void* pinned = dlopen_nid_postfix(argv[1], 2); add = reinterpret_cast(dlsym_nid_postfix(pinned, "GuestModuleAdd")); Require(add && _sceKernelRtldThreadAtexitIncrement_nid_postfix(reinterpret_cast(add)) == 0); @@ -222,9 +270,11 @@ int main(int argc, char** argv) { Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(add)) == 0); Require(_sceKernelRtldThreadAtexitIncrement_nid_postfix(reinterpret_cast(&ThrowsRuntimeError)) == 0); Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(&ThrowsRuntimeError)) == 0); - Require(ThrowsRuntimeError([] { _sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(&Require)); })); int local = 0; - Require(ThrowsRuntimeError([&] { _sceKernelRtldThreadAtexitIncrement_nid_postfix(&local); })); + Require(_sceKernelRtldThreadAtexitIncrement_nid_postfix(&local) == -1); + Require(_sceKernelRtldThreadAtexitIncrement_nid_postfix(nullptr) == -1); + Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(&local) == 0); + Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(nullptr) == 0); #ifndef _WIN32 Require(dlsym_nid_postfix(nullptr, "malloc") == nullptr); Require(dlsym_nid_postfix(nullptr, "dlopen") == nullptr); diff --git a/core/libs/tests/GuestEqueueEvents.cpp b/core/libs/tests/GuestEqueueEvents.cpp index 1a990f8c23..56e10c1485 100644 --- a/core/libs/tests/GuestEqueueEvents.cpp +++ b/core/libs/tests/GuestEqueueEvents.cpp @@ -23,16 +23,6 @@ static bool RejectsNull(TResult (APS5_VABI *accessor)(const KernelEvent*)) { return false; } -static bool RejectsWideFflags(const KernelEvent* ev) { - try { - sceKernelGetEventError(ev); - - } catch (const std::runtime_error&) { - return true; - } - return false; -} - static void SleepAtLeast(std::chrono::steady_clock::duration duration) { const auto start = std::chrono::steady_clock::now(); while (std::chrono::steady_clock::now() - start < duration) { @@ -327,7 +317,15 @@ int main() { Require(sceKernelGetEventFflags(&rawEvent) == static_cast(0x80000001LL)); Require(sceKernelGetEventData(&rawEvent) == -5); Require(sceKernelGetEventUserData(&rawEvent) == nullptr); - Require(RejectsWideFflags(&rawEvent)); + Require(sceKernelGetEventError(&rawEvent) == static_cast(0x00020001)); + const std::uint32_t wide[][2] = { + {0x00010000u, 0x80030000u}, {0x00020000u, 0x80040000u}, {0x7fff0001u, 0x00010001u}, + {0x80020005u, 0x00040005u}, {0x8000ffffu, 0x0002ffffu}, {0xffffffffu, 0x8001ffffu}, + }; + for (const auto& [fflags, error] : wide) { + rawEvent.fflags = fflags; + Require(sceKernelGetEventError(&rawEvent) == static_cast(error)); + } rawEvent.fflags = 0; rawEvent.flags = EV_ERROR; rawEvent.data = 22; diff --git a/core/libs/tests/GuestErrors.cpp b/core/libs/tests/GuestErrors.cpp index 1946147d26..25ac0bb23a 100644 --- a/core/libs/tests/GuestErrors.cpp +++ b/core/libs/tests/GuestErrors.cpp @@ -1,10 +1,12 @@ #include "prx/libc/include/WindowsFormatting.hpp" #include "prx/libc/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include #include +#include #include static void Require(bool value) { if (!value) std::abort(); } @@ -58,40 +60,51 @@ int main() { Require(std::strcmp(parent, "Invalid argument") == 0); char buffer[128]; for (int error = 0; error <= 96; ++error) { - Require(strerror_r_nid_postfix(error, buffer, sizeof(buffer)) == 0); - Require(std::strcmp(buffer, freebsd[error]) == 0); char coded[32]; std::snprintf(coded, sizeof(coded), "error_code=0x%08x", static_cast(error)); const char* expected = error == 0 ? "No error" : error < 95 ? freebsd[error] : coded; + Require(strerror_r_nid_postfix(error, buffer, sizeof(buffer)) == 0); + Require(std::strcmp(buffer, expected) == 0); Require(std::strcmp(strerror_nid_postfix(error), expected) == 0); Require(*__error_nid_postfix() == 13); } - Require(strerror_r_nid_postfix(-1, buffer, sizeof(buffer)) == 22); - Require(std::strcmp(buffer, "Unknown error: -1") == 0); - Require(*__error_nid_postfix() == 13); - struct UnknownError { - int error; - const char* text; - const char* coded; - }; - for (const UnknownError& unknown : {UnknownError{97, "Unknown error: 97", "error_code=0x00000061"}, - UnknownError{9999, "Unknown error: 9999", "error_code=0x0000270f"}, - UnknownError{INT_MAX, "Unknown error: 2147483647", "error_code=0x7fffffff"}, - UnknownError{-1, "Unknown error: -1", "error_code=0xffffffff"}, - UnknownError{INT_MIN, "Unknown error: -2147483648", "error_code=0x80000000"}}) { - Require(strerror_r_nid_postfix(unknown.error, buffer, sizeof(buffer)) == 22); - Require(std::strcmp(buffer, unknown.text) == 0); + for (const int error : {97, 100, 1000, 9999, INT_MAX, -1, INT_MIN}) { + char coded[32]; + std::snprintf(coded, sizeof(coded), "error_code=0x%08x", static_cast(error)); + Require(strerror_r_nid_postfix(error, buffer, sizeof(buffer)) == 0); + Require(std::strcmp(buffer, coded) == 0); Require(*__error_nid_postfix() == 13); - Require(std::strcmp(strerror_nid_postfix(unknown.error), unknown.coded) == 0); + Require(std::strcmp(strerror_nid_postfix(error), coded) == 0); Require(*__error_nid_postfix() == 13); } - char sentinel[] = "xyz"; - Require(strerror_r_nid_postfix(22, sentinel, 1) == 34); - Require(sentinel[0] == 0 && sentinel[1] == 'y'); - sentinel[0] = 'x'; - Require(strerror_r_nid_postfix(22, sentinel, 0) == 34 && sentinel[0] == 'x'); + for (const int error : {0, 19, 22, 95, -1}) { + for (const std::size_t length : {std::size_t{0}, std::size_t{1}, std::size_t{5}, std::size_t{16}, std::size_t{22}}) { + char canary[64]; + std::memset(canary, 0xcc, sizeof(canary)); + Require(strerror_r_nid_postfix(error, canary, length) == 34); + for (const char byte : canary) Require(static_cast(byte) == 0xcc); + Require(*__error_nid_postfix() == 13); + } + char smallest[64]; + std::memset(smallest, 0xcc, sizeof(smallest)); + Require(strerror_r_nid_postfix(error, smallest, 23) == 0); + const char* text = strerror_nid_postfix(error); + const std::size_t copied = std::min(std::strlen(text), 22); + Require(std::memcmp(smallest, text, copied) == 0); + for (std::size_t index = copied; index < 23; ++index) Require(smallest[index] == 0); + Require(static_cast(smallest[23]) == 0xcc); + Require(*__error_nid_postfix() == 13); + } + char truncated[32]; + Require(strerror_r_nid_postfix(19, truncated, 23) == 0); + Require(std::strcmp(truncated, "Operation not supporte") == 0); Require(strerror_r_nid_postfix(22, nullptr, 0) == 34); - char truncated[4]; - Require(strerror_r_nid_postfix(97, truncated, sizeof(truncated)) == 22 && std::strcmp(truncated, "Unk") == 0); - Require(strerror_r_nid_postfix(-1, truncated, sizeof(truncated)) == 22 && std::strcmp(truncated, "Unk") == 0); + Require(strerror_r_nid_postfix(22, nullptr, 22) == 34); + bool threw = false; + try { + strerror_r_nid_postfix(22, nullptr, 23); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); } diff --git a/core/libs/tests/GuestExecHost.cpp b/core/libs/tests/GuestExecHost.cpp new file mode 100644 index 0000000000..959a1f35c6 --- /dev/null +++ b/core/libs/tests/GuestExecHost.cpp @@ -0,0 +1,227 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libkernel/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + + +namespace { + +bool cleaned = false; + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "Exec fixture: %s\n", message); + std::fflush(stderr); + std::_Exit(98); + } +} + +std::filesystem::path Root() { +#ifdef _WIN32 + std::wstring value(32768, L'\0'); + const auto length = GetEnvironmentVariableW(L"ANYPS5_LOADEXEC_TEST_ROOT", value.data(), static_cast(value.size())); + Require(length > 0 && length < value.size(), "missing fixture directory"); + value.resize(length); + return value; +#else + const auto* value = std::getenv("ANYPS5_LOADEXEC_TEST_ROOT"); + Require(value != nullptr, "missing fixture directory"); + return std::filesystem::path(reinterpret_cast(value)); +#endif +} + +void RecordPid() { + std::ofstream file(Root() / "pids.txt", std::ios::app); +#ifdef _WIN32 + file << GetCurrentProcessId() << '\n'; +#else + file << getpid() << '\n'; +#endif + file.close(); + Require(!file.fail(), "writing the process ID file"); +} + +void Record(const char* value) { + std::ofstream file(Root() / "events.txt", std::ios::app); + file << value << '\n'; + file.close(); + Require(!file.fail(), "writing the event file"); +} + +std::string Events() { + std::ifstream file(Root() / "events.txt"); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +void Shutdown() { cleaned = true; Record("shutdown"); } +void APS5_VABI Finalize(void*) { Record("guest-exit"); } +void HostFinalize() { Record("host-exit"); } + +void Register() { + LibcRegisterShutdown_nid_postfix(Shutdown); + Require(__cxa_atexit_nid_postfix(Finalize, nullptr, nullptr) == 0, "guest exit registration"); + Require(std::atexit(HostFinalize) == 0, "host exit registration"); +} + +void Arguments(const std::uintptr_t* block, const std::vector& expected) { + Require(block[0] == expected.size(), "entry argument count"); + const auto* argv = reinterpret_cast(block + 1); + Require(getargc_nid_postfix() == static_cast(expected.size()), "getargc argument count"); + const auto* kernel = getargv_nid_postfix(); + for (std::size_t index = 0; index < expected.size(); ++index) { + Require(argv[index] && expected[index] == argv[index], "entry argument value"); + Require(kernel[index] && expected[index] == kernel[index], "getargv argument value"); + } + Require(argv[expected.size()] == nullptr && kernel[expected.size()] == nullptr, "argument terminator"); +} + +std::string LoaderEnvironment() { + const auto* value = std::getenv("LD_LIBRARY_PATH"); + return value != nullptr ? std::string("LD_LIBRARY_PATH=") + value : std::string(); +} + +std::vector SecondEnvironment() { + std::vector entries{"EXEC_SECOND=stage2"}; + auto loader = LoaderEnvironment(); + if (!loader.empty()) entries.push_back(loader); + const auto* root = std::getenv("ANYPS5_LOADEXEC_TEST_ROOT"); + Require(root != nullptr, "missing fixture directory for the environment"); + entries.emplace_back(std::string("ANYPS5_LOADEXEC_TEST_ROOT=") + root); + return entries; +} + +} + +extern "C" void APS5_VABI ExecFirst_nid_no_patch(const std::uintptr_t* block) { + (void)block; + RecordPid(); + Record("first"); + Register(); + char* const present[] = {const_cast("/app0/picker/second.bin"), nullptr}; + char* const empty[] = {nullptr}; + char* const unpaired[] = {const_cast("EXEC_SECOND"), nullptr}; + const char* const absent = "/app0/picker/absent.bin"; + Require(execv_nid_postfix(nullptr, present) == -1 && *__error_nid_postfix() == 9, "null path rejected with EBADF"); + Require(execv_nid_postfix("/app0/picker/second.bin", nullptr) == -1 && *__error_nid_postfix() == 14, + "null arguments rejected with EFAULT"); + Require(execv_nid_postfix(absent, empty) == -1 && *__error_nid_postfix() == 2, + "an empty argument list is accepted and an absent path rejected with ENOENT"); + Require(execve_nid_postfix(absent, present, nullptr) == -1 && *__error_nid_postfix() == 2, + "a null environment is accepted and an absent path rejected with ENOENT"); + Require(execve_nid_postfix(absent, present, unpaired) == -1 && *__error_nid_postfix() == 2, + "an unpaired environment entry is copied and an absent path rejected with ENOENT"); + Require(execvp_nid_postfix("absent-bin", present) == -1 && *__error_nid_postfix() == 2, "PATH search ends with ENOENT"); + static std::string huge(300000, 'X'); + char* const oversized[] = {const_cast("/app0/picker/second.bin"), &huge[0], nullptr}; + Require(execv_nid_postfix("/app0/picker/second.bin", oversized) == -1 && *__error_nid_postfix() == 7, + "oversized arguments rejected with E2BIG"); + static std::string largeVector(262201, 'K'); + static std::string largeEntry = "EXEC_LARGE_SECOND=" + largeVector; + char* const largeEnvironment[] = {&largeEntry[0], nullptr}; + Require(execve_nid_postfix(absent, present, largeEnvironment) == -1 && *__error_nid_postfix() == 7, + "an environment vector beyond the limit rejected with E2BIG"); + char* const beyond[] = {const_cast("/app0/picker/second.bin/child"), nullptr}; + Require(execv_nid_postfix("/app0/picker/second.bin/child", beyond) == -1 && *__error_nid_postfix() == 20, + "a path beyond a regular file rejected with ENOTDIR"); +#ifndef _WIN32 + char* const stripped[] = {const_cast("/app0/picker/noexec.bin"), nullptr}; + Require(execv_nid_postfix("/app0/picker/noexec.bin", stripped) == -1 && *__error_nid_postfix() == 13, + "a regular file without execute permission rejected with EACCES"); +#endif +#ifdef _WIN32 + const auto second = Root() / L"picker.second.bin.exe"; +#else + const auto second = Root() / "picker.second.bin.elf"; +#endif + auto intact = second; + intact += ".intact"; + std::filesystem::rename(second, intact); + { + std::ofstream broken(second, std::ios::binary); + broken << "no image"; + broken.close(); + Require(execv_nid_postfix("/app0/picker/second.bin", present) == -1 && *__error_nid_postfix() == 8, + "a converted target without an executable image rejected with ENOEXEC"); + } + std::filesystem::remove(second); + std::filesystem::rename(intact, second); + auto saved = second; + saved += ".saved"; + std::filesystem::rename(second, saved); + bool rejected = false; + try { + execv_nid_postfix("/app0/picker/second.bin", present); + } catch (const std::runtime_error&) { + rejected = true; + } + std::filesystem::rename(saved, second); + Require(rejected && Events() == "first\n", "missing conversion rejected without replacing"); + std::filesystem::current_path(Root() / "app0" / "working"); + Record("launch"); + std::vector values = SecondEnvironment(); + std::vector environment; + for (auto& value : values) environment.push_back(&value[0]); + environment.push_back(nullptr); + char* const target[] = {const_cast("/app0/picker/second.bin"), const_cast("argument with spaces"), + const_cast("quote\"and\\slash\\ Кириллица 東京"), nullptr}; + try { + execve_nid_postfix("/app0/picker/second.bin", target, environment.data()); + } catch (...) { + Record("throw"); + std::fflush(nullptr); + std::_Exit(96); + } + Require(false, "execve returned to the first executable"); +} + +extern "C" void APS5_VABI ExecSecond_nid_no_patch(const std::uintptr_t* block) { + Arguments(block, {"/app0/picker/second.bin", "argument with spaces", "quote\"and\\slash\\ Кириллица 東京"}); +#ifdef _WIN32 + Require(std::filesystem::equivalent(std::filesystem::current_path(), Root()), "secondary inherits the image directory"); +#else + Require(std::filesystem::equivalent(std::filesystem::current_path(), Root() / "app0" / "working"), "exec keeps the working directory"); +#endif + Require(Events() == "first\nlaunch\n", "exec replaced the image without running exit handlers"); + const std::vector entries{SecondEnvironment()}; +#ifdef _WIN32 + char value[64]{}; + Require(GetEnvironmentVariableA("EXEC_SECOND", value, 64) == 6 && value == std::string("stage2"), + "execve environment entry reached the replacement"); + Require(GetEnvironmentVariableA("PATH", value, 64) != 0, "execve keeps the inherited host variables"); + Require(GetEnvironmentVariableA("ANYPS5_LOADEXEC_PARENT", value, 64) == 0, "parent wait variable was retained"); + Require(GetEnvironmentVariableA("ANYPS5_LOADEXEC_ARGV_SKIP", value, 64) == 0, "argument marker was retained"); +#else + extern char** environ; + Require(environ[0] != nullptr, "replacement environment present"); + for (const auto& entry : entries) { + bool found = false; + for (std::size_t index = 0; environ[index] != nullptr; ++index) + found = found || std::strcmp(environ[index], entry.c_str()) == 0; + Require(found, "replacement environment entry value"); + } + bool hostPath = false; + for (std::size_t index = 0; environ[index] != nullptr; ++index) + hostPath = hostPath || std::strncmp(environ[index], "PATH=", 5) == 0; + Require(hostPath, "exec keeps the inherited host variables"); + Require(!cleaned, "secondary inherited old runtime state"); +#endif + RecordPid(); + Record("second"); + std::fflush(nullptr); + std::_Exit(37); +} diff --git a/core/libs/tests/GuestExecvp.cpp b/core/libs/tests/GuestExecvp.cpp index 22e67f4d98..a471a33087 100644 --- a/core/libs/tests/GuestExecvp.cpp +++ b/core/libs/tests/GuestExecvp.cpp @@ -1,26 +1,34 @@ #include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/Api.hpp" #include "prx/libc/include/General.hpp" #include -#include #include -template static bool Throws(TCall call, const std::string& message) { - try { - call(); - } catch (const std::runtime_error& error) { - return error.what() == message; - } - return false; +static void Require(bool value) { + if (!value) std::abort(); } -static void Require(bool value) { if (!value) std::abort(); } + int main() { - char name[] = "sh"; + char name[] = "EXECVP_ABSENT_BINARY"; char* arguments[] = {name, nullptr}; - char variable[] = "HOME=/"; - char* environment[] = {variable, nullptr}; - Require(Throws([&] { execvp_nid_postfix("sh", arguments); }, "execvp: executable replacement not implemented")); - Require(Throws([&] { execvp_nid_postfix("/bin/sh", arguments); }, "execvp: executable replacement not implemented")); - Require(Throws([&] { execvp_nid_postfix(nullptr, nullptr); }, "execvp: executable replacement not implemented")); - Require(Throws([&] { execv_nid_postfix("/bin/sh", arguments); }, "execv: executable replacement not implemented")); - Require(Throws([&] { execve_nid_postfix("/bin/sh", arguments, environment); }, "execve: executable replacement not implemented")); + char unpairedEntry[] = "EXECVP_SECOND_STAGE"; + char* unpaired[] = {unpairedEntry, nullptr}; + std::string huge(300000, 'X'); + char* oversized[] = {name, &huge[0], nullptr}; + char* const empty[] = {nullptr}; + static std::string firstVector(132000, 'K'); + static std::string secondVector(132000, 'M'); + std::string firstEntry = "EXECVP_LARGE_FIRST=" + firstVector; + std::string secondEntry = "EXECVP_LARGE_SECOND=" + secondVector; + char* largeEnvironment[] = {&firstEntry[0], &secondEntry[0], nullptr}; + Require(execvp_nid_postfix(nullptr, nullptr) == -1 && *__error_nid_postfix() == 9); + Require(execv_nid_postfix(nullptr, arguments) == -1 && *__error_nid_postfix() == 9); + Require(execv_nid_postfix(name, nullptr) == -1 && *__error_nid_postfix() == 14); + Require(execv_nid_postfix("", arguments) == -1 && *__error_nid_postfix() == 2); + Require(execv_nid_postfix(name, empty) == -1 && *__error_nid_postfix() == 2); + Require(execve_nid_postfix(name, arguments, nullptr) == -1 && *__error_nid_postfix() == 2); + Require(execve_nid_postfix(name, arguments, unpaired) == -1 && *__error_nid_postfix() == 2); + Require(execvp_nid_postfix("EXECVP_NO_SUCH_LIBRARY", arguments) == -1 && *__error_nid_postfix() == 2); + Require(execv_nid_postfix(name, oversized) == -1 && *__error_nid_postfix() == 7); + Require(execve_nid_postfix(name, arguments, largeEnvironment) == -1 && *__error_nid_postfix() == 7); } diff --git a/core/libs/tests/GuestFileTimestamps.cpp b/core/libs/tests/GuestFileTimestamps.cpp index 983915404c..e45df8c8f5 100644 --- a/core/libs/tests/GuestFileTimestamps.cpp +++ b/core/libs/tests/GuestFileTimestamps.cpp @@ -226,7 +226,14 @@ int main() { Require(utimes_nid_postfix(path.c_str(), second) == 0, "utimes target to second"); std::error_code symlinkError; const auto linkPath = (root / "link.txt").string(); +#ifdef _WIN32 + if (!CreateSymbolicLinkW(std::filesystem::path(linkPath).c_str(), std::filesystem::path(path).c_str(), + SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE)) { + symlinkError = std::error_code(GetLastError(), std::system_category()); + } +#else std::filesystem::create_symlink(path, linkPath, symlinkError); +#endif if (!symlinkError) { Require(lutimes_nid_postfix(linkPath.c_str(), first) == 0, "lutimes modifies symlink"); FileStat targetStatus{}; diff --git a/core/libs/tests/GuestFileVector.cpp b/core/libs/tests/GuestFileVector.cpp index 72e398204c..b60d388465 100644 --- a/core/libs/tests/GuestFileVector.cpp +++ b/core/libs/tests/GuestFileVector.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -279,12 +280,22 @@ int main() { const int readOnly = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDONLY, 0); Require(readOnly >= 0); Require(sceKernelPwrite(readOnly, "X", 1, 0) == ErrorEbadf); + Require(sceKernelPwrite(readOnly, "X", 1, -1) == ErrorEbadf); + Require(pwrite_nid_disambig1_nid_postfix(readOnly, nullptr, 1, 0) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelPwritev(readOnly, writes, 2, -1) == ErrorEbadf); + Require(pwritev_nid_postfix(readOnly, writes, 2, -1) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelPreadv(readOnly, reads, 2, -1) == ErrorEinval); Require(sceKernelFtruncate(readOnly, 0) == ErrorEinval); Require(sceKernelClose(readOnly) == 0); const int writeOnly = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_WRONLY, 0); Require(writeOnly >= 0); Require(sceKernelPread(writeOnly, &byte, 1, 0) == ErrorEbadf); Require(pread_nid_postfix(writeOnly, &byte, 1, 0) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelPread(writeOnly, &byte, 1, -1) == ErrorEbadf); + Require(pread_nid_postfix(writeOnly, nullptr, 1, 0) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelPreadv(writeOnly, reads, 2, -1) == ErrorEbadf); + Require(preadv_nid_postfix(writeOnly, reads, 2, -1) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelPwritev(writeOnly, writes, 2, -1) == ErrorEinval); Require(sceKernelClose(writeOnly) == 0); Require(Contents(path) == "01ABCDE7ABCDE"); Require(sceKernelFtruncate(file, 5) == 0); @@ -298,6 +309,17 @@ int main() { Require(sceKernelPread(file, &byte, 1, 0) == ErrorEbadf); Require(sceKernelPwrite(file, &byte, 1, 0) == ErrorEbadf); Require(sceKernelFtruncate(file, 0) == ErrorEbadf); + Require(sceKernelPreadv(file, reads, 2, -1) == ErrorEbadf); + Require(sceKernelPwritev(file, writes, 2, -1) == ErrorEbadf); + Require(preadv_nid_postfix(file, reads, 2, -1) == -1 && *__error_nid_postfix() == 9); + Require(pwritev_nid_postfix(file, writes, 2, -1) == -1 && *__error_nid_postfix() == 9); + KernelIovec oversized[2] = {{first, static_cast(std::numeric_limits::max())}, {second, 1}}; + Require(sceKernelReadv(file, oversized, 2) == ErrorEinval); + Require(sceKernelWritev(file, oversized, 2) == ErrorEinval); + Require(sceKernelPreadv(file, oversized, 2, 0) == ErrorEinval); + Require(sceKernelPwritev(file, oversized, 2, 0) == ErrorEinval); + Require(readv_nid_postfix(file, oversized, 2) == -1 && *__error_nid_postfix() == 22); + Require(pwritev_nid_postfix(file, oversized, 2, 0) == -1 && *__error_nid_postfix() == 22); int ends[2] = {}; Require(MakePipe(ends) == 0); @@ -317,6 +339,10 @@ int main() { Require(sceKernelReadv(ends[0], reads, 2) == 3 && std::memcmp(first, "uvw", 3) == 0); Require(sceKernelPreadv(ends[0], reads, 2, 0) == ErrorEspipe); Require(sceKernelPwritev(ends[1], message, 1, 0) == ErrorEspipe); + Require(sceKernelPreadv(ends[0], reads, 2, -1) == ErrorEspipe); + Require(sceKernelPwritev(ends[1], message, 1, -1) == ErrorEspipe); + Require(preadv_nid_postfix(ends[0], reads, 2, -1) == -1 && *__error_nid_postfix() == 29); + Require(pwritev_nid_postfix(ends[1], message, 1, -1) == -1 && *__error_nid_postfix() == 29); #ifndef _WIN32 Require(::fcntl(ends[0], F_SETFL, ::fcntl(ends[0], F_GETFL) | O_NONBLOCK) == 0); Require(sceKernelReadv(ends[0], reads, 2) == ErrorEagain); @@ -324,6 +350,18 @@ int main() { #endif Require(ClosePipe(ends[0]) == 0 && ClosePipe(ends[1]) == 0); + const int queue = kqueue_nid_postfix(); + Require(queue >= 0); + Require(sceKernelPreadv(queue, reads, 2, -1) == ErrorEspipe); + Require(sceKernelPwritev(queue, writes, 2, -1) == ErrorEspipe); + Require(preadv_nid_postfix(queue, reads, 2, -1) == -1 && *__error_nid_postfix() == 29); + Require(pwritev_nid_postfix(queue, writes, 2, -1) == -1 && *__error_nid_postfix() == 29); + Require(sceKernelPreadv(queue, reads, 2, 0) == ErrorEspipe); + Require(sceKernelPwritev(queue, writes, 2, 0) == ErrorEspipe); + Require(sceKernelPread(queue, &byte, 1, 0) == ErrorEspipe); + Require(pwrite_nid_disambig1_nid_postfix(queue, &byte, 1, -1) == -1 && *__error_nid_postfix() == 29); + Require(sceKernelClose(queue) == 0); + const int random = sceKernelOpen("/dev/urandom", 0, 0); Require(random >= 0); unsigned char noise[48]{}; diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index b2bf77d497..043a147a54 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -255,6 +255,7 @@ static void TestRelinkedModuleNames() { const auto relinked = original + ".guest.prx"; FileStat status{}; Require(sceKernelStat(original.c_str(), &status) == static_cast(0x80020002u)); + Require(lstat_nid_postfix(original.c_str(), &status) == -1 && *__error_nid_postfix() == 2); Require(sceKernelCheckReachability(original.c_str()) == static_cast(0x80020002u)); Require(sceKernelOpen(original.c_str(), 0, 0) == static_cast(0x80020002u)); const auto missing = (root / "missing-content.pak").string(); @@ -267,6 +268,7 @@ static void TestRelinkedModuleNames() { { std::ofstream file(relinked); file << "relinked"; } Require(sceKernelStat(original.c_str(), &status) == 0 && status.st_size == 8); Require(stat_nid_postfix(original.c_str(), &status) == 0 && status.st_size == 8); + Require(lstat_nid_postfix(original.c_str(), &status) == 0 && status.st_size == 8); Require(sceKernelCheckReachability(original.c_str()) == 0); const int opened = sceKernelOpen(original.c_str(), 0, 0); Require(opened >= 0 && sceKernelClose(opened) == 0); @@ -632,6 +634,27 @@ int main() { Require(chflags_nid_postfix(file.string().c_str(), 0x200000) == -1 && *__error_nid_postfix() == 1); Require(ChflagsThrows(file, 0x2, "UF_IMMUTABLE")); Require(ChflagsThrows(file, 0x1000 | 0x40000, "UF_READONLY|SF_APPEND")); + const auto chflagsDangling = root / "dangling.link"; + const auto chflagsPointing = root / "file.link"; +#ifdef _WIN32 + const bool chflagsLinked = CreateSymbolicLinkW(chflagsDangling.wstring().c_str(), L"missing.bin", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0 && + CreateSymbolicLinkW(chflagsPointing.wstring().c_str(), L"file.txt", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0; +#else + std::error_code chflagsLinkError; + std::filesystem::create_symlink("missing.bin", chflagsDangling, chflagsLinkError); + if (!chflagsLinkError) std::filesystem::create_symlink("file.txt", chflagsPointing, chflagsLinkError); + const bool chflagsLinked = !chflagsLinkError; +#endif + if (chflagsLinked) { + Require(chflags_nid_postfix(chflagsDangling.string().c_str(), 0) == -1 && *__error_nid_postfix() == 2); + Require(chflags_nid_postfix(chflagsPointing.string().c_str(), 0) == 0); + Require(chflags_nid_postfix((chflagsPointing.string() + "/").c_str(), 0) == -1 && *__error_nid_postfix() == 20); + Require(chflags_nid_postfix(chflagsPointing.string().c_str(), 0x20) == -1 && *__error_nid_postfix() == 45); + Require(unlink_nid_postfix(chflagsDangling.string().c_str()) == 0 && unlink_nid_postfix(chflagsPointing.string().c_str()) == 0); + } else { + unlink_nid_postfix(chflagsDangling.string().c_str()); + std::fprintf(stderr, "symbolic links unavailable: the chflags link cases not tested\n"); + } const int flagged = open_nid_postfix(file.string().c_str(), 0, 0); Require(flagged >= 0 && fchflags_nid_postfix(flagged, 0) == 0); Require(fchflags_nid_postfix(flagged, 0x20) == -1 && *__error_nid_postfix() == 45); @@ -650,6 +673,27 @@ int main() { Require(close_nid_postfix(flaggedKqueue) == 0); const int flaggedRoot = sceKernelOpen(root.string().c_str(), 0x20000, 0); Require(flaggedRoot >= 0 && fchflags_nid_postfix(flaggedRoot, 0) == 0 && sceKernelClose(flaggedRoot) == 0); + const auto exclusiveLink = root / "exclusive.link"; + const auto exclusiveTarget = root / "exclusive-target.txt"; +#ifdef _WIN32 + const bool exclusiveLinked = CreateSymbolicLinkW(exclusiveLink.wstring().c_str(), L"exclusive-target.txt", + SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0; +#else + std::error_code exclusiveLinkError; + std::filesystem::create_symlink("exclusive-target.txt", exclusiveLink, exclusiveLinkError); + const bool exclusiveLinked = !exclusiveLinkError; +#endif + if (exclusiveLinked) { + Require(open_nid_postfix(exclusiveLink.string().c_str(), 0xa01, 0644) == -1 && *__error_nid_postfix() == 17); + Require(sceKernelOpen(exclusiveLink.string().c_str(), 0xa01, 0644) == static_cast(0x80020011u)); + Require(!std::filesystem::exists(exclusiveTarget)); + const int exclusiveCreated = open_nid_postfix(exclusiveLink.string().c_str(), 0x201, 0644); + Require(exclusiveCreated >= 0 && close_nid_postfix(exclusiveCreated) == 0 && std::filesystem::is_regular_file(exclusiveTarget)); + Require(open_nid_postfix(exclusiveLink.string().c_str(), 0xa01, 0644) == -1 && *__error_nid_postfix() == 17); + Require(unlink_nid_postfix(exclusiveLink.string().c_str()) == 0 && unlink_nid_postfix(exclusiveTarget.string().c_str()) == 0); + } else { + std::fprintf(stderr, "symbolic links unavailable: the O_EXCL link cases not tested\n"); + } int flaggedPipe[2]; Require(pipe_nid_postfix(flaggedPipe) == 0); Require(fchflags_nid_postfix(flaggedPipe[0], 0) == -1 && *__error_nid_postfix() == 22); @@ -847,6 +891,37 @@ int main() { }; Require(sceKernelFchmod(descriptor, 0400) == 0 && !ownerWrite()); Require(fchmod_nid_postfix(descriptor, 0600) == 0 && ownerWrite()); + const auto sizedLink = sized.parent_path() / "sized.link"; + const auto missingLink = sized.parent_path() / "missing.link"; + const auto loopLink = sized.parent_path() / "loop.link"; +#ifdef _WIN32 + const DWORD linkFlags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; + const bool chmodLinked = CreateSymbolicLinkW(sizedLink.wstring().c_str(), sized.filename().wstring().c_str(), linkFlags) && + CreateSymbolicLinkW(missingLink.wstring().c_str(), L"missing.bin", linkFlags) && + CreateSymbolicLinkW(loopLink.wstring().c_str(), L"loop.link", linkFlags); +#else + std::error_code chmodLinkError; + std::filesystem::create_symlink(sized.filename(), sizedLink, chmodLinkError); + if (!chmodLinkError) std::filesystem::create_symlink("missing.bin", missingLink, chmodLinkError); + if (!chmodLinkError) std::filesystem::create_symlink("loop.link", loopLink, chmodLinkError); + const bool chmodLinked = !chmodLinkError; +#endif + if (chmodLinked) { + Require(chmod_nid_postfix(sizedLink.string().c_str(), 0400) == 0 && !ownerWrite()); +#ifdef _WIN32 + Require((GetFileAttributesW(sizedLink.wstring().c_str()) & FILE_ATTRIBUTE_READONLY) == 0); +#endif + Require(chmod_nid_postfix(sizedLink.string().c_str(), 0600) == 0 && ownerWrite()); + Require(chmod_nid_postfix(missingLink.string().c_str(), 0600) == -1 && *__error_nid_postfix() == 2); +#ifdef _WIN32 + Require(chmod_nid_postfix(loopLink.string().c_str(), 0600) == -1 && *__error_nid_postfix() == 62); +#endif + } else { + std::fprintf(stderr, "symbolic links unavailable: the chmod link cases not tested\n"); + } + unlink_nid_postfix(sizedLink.string().c_str()); + unlink_nid_postfix(missingLink.string().c_str()); + unlink_nid_postfix(loopLink.string().c_str()); Require(sceKernelFtruncate(descriptor, 3) == 0); FileStat times{}; const KernelTimeval past[2]{{1000000000, 0}, {1000000000, 500000}}; @@ -884,6 +959,8 @@ int main() { Require(fsync_nid_postfix(end) == -1 && *__error_nid_postfix() == 22); Require(fdatasync_nid_postfix(end) == -1 && *__error_nid_postfix() == 22); Require(sceKernelFsync(end) == static_cast(0x80020016u) && sceKernelFdatasync(end) == static_cast(0x80020016u)); + Require(fchmod_nid_postfix(end, 0600) == -1 && *__error_nid_postfix() == 22); + Require(sceKernelFchmod(end, 0600) == static_cast(0x80020016u)); } Require(close_nid_postfix(syncPipe[0]) == 0 && close_nid_postfix(syncPipe[1]) == 0); Require(close_nid_postfix(socket) == 0); @@ -1042,6 +1119,18 @@ int main() { Require(stat_nid_postfix(fileLink.string().c_str(), &followed) == 0 && (followed.st_mode & 0170000) == 0100000); Require(followed.st_ino == status.st_ino && linkStatus.st_ino != status.st_ino && linkStatus.st_dev == status.st_dev); Require(stat_nid_postfix(directoryLink.string().c_str(), &followed) == 0 && (followed.st_mode & 0170000) == 0040000); + const auto renameThroughLoop = statLoop.string() + "\\inside"; + const auto renameTarget = (root / "rename-target").string(); + Require(sceKernelRename(renameThroughLoop.c_str(), renameTarget.c_str()) == static_cast(0x8002003eu)); + Require(rename_nid_postfix(renameThroughLoop.c_str(), renameTarget.c_str()) == -1 && *__error_nid_postfix() == 62); + Require(sceKernelRename(present.string().c_str(), renameThroughLoop.c_str()) == static_cast(0x8002003eu)); + Require(rename_nid_postfix(present.string().c_str(), renameThroughLoop.c_str()) == -1 && *__error_nid_postfix() == 62); + const auto missingSource = (root / "missing-source").string(); + const auto underFile = present.string() + "\\child"; + Require(sceKernelRename(missingSource.c_str(), renameThroughLoop.c_str()) == static_cast(0x80020002u)); + Require(rename_nid_postfix(missingSource.c_str(), renameThroughLoop.c_str()) == -1 && *__error_nid_postfix() == 2); + Require(sceKernelRename(underFile.c_str(), renameThroughLoop.c_str()) == static_cast(0x80020014u)); + Require(rename_nid_postfix(underFile.c_str(), renameThroughLoop.c_str()) == -1 && *__error_nid_postfix() == 20); Require(stat_nid_postfix(fileLink.string().c_str(), &status) == 0 && status.st_size == 5); FileStat absoluteStatus{}; Require(lstat_nid_postfix(absoluteLink.string().c_str(), &absoluteStatus) == 0); @@ -1238,6 +1327,11 @@ int main() { Require(sceKernelPread(directoryDescriptor, directoryBytes, sizeof(directoryBytes), 0) == static_cast(0x80020015u)); Require(sceKernelPreadv(directoryDescriptor, directoryVector, 2, 0) == static_cast(0x80020015u)); Require(sceKernelPwrite(directoryDescriptor, "x", 1, 0) == static_cast(0x80020009u)); + Require(sceKernelWrite(directoryDescriptor, "x", 1) == static_cast(0x80020009u)); + Require(write_nid_postfix(directoryDescriptor, "x", 1) == -1 && *__error_nid_postfix() == 9); + Require(pwrite_nid_disambig1_nid_postfix(directoryDescriptor, "x", 1, 0) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelWritev(directoryDescriptor, directoryVector, 2) == static_cast(0x80020009u)); + Require(sceKernelPwritev(directoryDescriptor, directoryVector, 2, 0) == static_cast(0x80020009u)); std::vector listing(4096); Require(sceKernelGetdents(directoryDescriptor, listing.data(), static_cast(listing.size())) > 0); const int plainFile = open_nid_postfix(presentName.c_str(), 0, 0); diff --git a/core/libs/tests/GuestFirmwareConstantReturns.cpp b/core/libs/tests/GuestFirmwareConstantReturns.cpp new file mode 100644 index 0000000000..f88525c988 --- /dev/null +++ b/core/libs/tests/GuestFirmwareConstantReturns.cpp @@ -0,0 +1,81 @@ +#include "SceTypes.hpp" +#include "prx/libSceComposite/libSceComposite.hpp" +#include "prx/libSceFsInternalForVsh/libSceFsInternalForVsh.hpp" +#include "prx/libSceHttpCache/libSceHttpCache.hpp" +#include "prx/libScePlayReady/libScePlayReady.hpp" +#include "prx/libSceSdma/libSceSdma.hpp" +#include "prx/libSceSsl/libSceSsl.hpp" +#include "prx/libSceVisionManager/libSceVisionManager.hpp" +#include "prx/libmdbg_syscore/libmdbg_syscore.hpp" +#include + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +void CheckComposite() { + constexpr int Unsupported = static_cast(0x80d40006); + Require(sceCompositorCreateIndirectRenderTarget() == Unsupported); + Require(sceCompositorDeleteIndirectRenderTarget() == Unsupported); + Require(sceCompositorSetIndirectCanvasCommand() == 0); + Require(sceCompositorSetIndirectCompositionCommand() == 0); + Require(sceCompositorSetIndirectRenderTargetConfigCommand() == 0); +} + +void CheckFsInternal() { + constexpr int KernelNotSupported = static_cast(0x8002002d); + Require(sceFsErriCtl() == KernelNotSupported); + Require(sceFsErriGetState() == KernelNotSupported); +} + +void CheckHttpCacheAndPlayReady() { + Require(sceHttpCacheDebugSetUrl() == 0); + Require(scePlayReadyMemGetStats() == static_cast(0x80004001)); +} + +void CheckSdma() { + constexpr int SdmaError = static_cast(0x809900c9); + Require(sceSdmaInitialize() == 0); + Require(sceSdmaFinalize() == 0); + Require(sceSdmaQueryWorkingBufferSize() == 0x8000); + Require(sceSdmaFlushAllWithEvent() == SdmaError); + Require(sceSdmaWaitForEvent() == SdmaError); +} + +void CheckSsl() { + Require(CRYPTO_uninitAsymmetricKey() == 0); + Require(VLONG_freeVlongQueue() == 0); +} + +void CheckVisionManager() { + constexpr int VisionError = static_cast(0x80e70004); + Require(sceVisionManagerOpen() == VisionError); + Require(sceVisionManagerClose() == VisionError); + Require(sceVisionManagerRequestFindUser() == VisionError); + Require(sceVisionManagerRequestFindUserInRegion() == VisionError); + Require(sceVisionManagerRequestFindUserWithoutIdentify() == VisionError); + Require(sceVisionManagerRequestRegisterUserInRegion() == VisionError); + Require(sceVisionManagerGetResultOfFindUser() == VisionError); + Require(sceVisionManagerGetResultOfFindUserInRegion() == VisionError); + Require(sceVisionManagerGetResultOfFindUserWithoutIdentify() == VisionError); + Require(sceVisionManagerGetResultOfRegisterUserInRegion() == VisionError); + Require(sceVisionManagerLoadSettings() == 0); +} + +void CheckDebugSyscore() { + Require(sceDebugGetProcessEventCntlFlag() == 1); + Require(sceDebugSetProcessEventCntlFlag() == 1); +} + +} + +int main() { + CheckComposite(); + CheckFsInternal(); + CheckHttpCacheAndPlayReady(); + CheckSdma(); + CheckSsl(); + CheckVisionManager(); + CheckDebugSyscore(); + return 0; +} diff --git a/core/libs/tests/GuestFloatingPoint.cpp b/core/libs/tests/GuestFloatingPoint.cpp index eb01ae2bc7..6d9afd5ae6 100644 --- a/core/libs/tests/GuestFloatingPoint.cpp +++ b/core/libs/tests/GuestFloatingPoint.cpp @@ -37,6 +37,10 @@ void WriteEnvironment(const Environment& state) { _mm_setcsr(state.sse); } +bool SameControlState(const Environment& left, const Environment& right) { + return left.x87[0] == right.x87[0] && left.x87[1] == right.x87[1] && left.sse == right.sse; +} + void Require(bool value, const char* message) { if (!value) { std::fprintf(stderr, "floating-point environment: %s\n", message); @@ -285,7 +289,7 @@ void CheckHoldExcept(HoldExcept hold) { try { hold(nullptr); } catch (const std::invalid_argument&) { threw = true; } Require(threw, "feholdexcept rejects null storage"); const auto afterNull = ReadEnvironment(); - Require(std::memcmp(&original, &afterNull, sizeof(original)) == 0, "null feholdexcept preserves the environment"); + Require(SameControlState(original, afterNull), "null feholdexcept preserves the environment"); #ifdef _WIN32 SYSTEM_INFO info{}; GetSystemInfo(&info); @@ -328,7 +332,7 @@ void CheckHoldExcept(HoldExcept hold) { first.join(); second.join(); const auto afterThreads = ReadEnvironment(); - Require(std::memcmp(&original, &afterThreads, sizeof(original)) == 0, "feholdexcept workers preserve the main environment"); + Require(SameControlState(original, afterThreads), "feholdexcept workers preserve the main environment"); } int main(int argc, char** argv) { diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index 6f46ad3ee6..093c88fe45 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -87,6 +87,25 @@ static std::int32_t APS5_VABI ParseText(FontTextSource* source, void** order, Fo return 1; } +struct ParsedCodes { + const std::uint32_t* codes; + std::size_t count; + std::size_t at; + FontHandle font; +}; + +static std::int32_t APS5_VABI ParseCodes(FontTextSource* source, void** order, FontTextParseResult* result) { + auto* parsed = static_cast(source->textObject); + *order = const_cast(parsed->codes + parsed->at); + if (parsed->at == parsed->count) { + result->Terminate.terminateCode = 0; + return 0; + } + result->FontCode.font = parsed->font; + result->FontCode.code = parsed->codes[parsed->at++]; + return 1; +} + static void CheckTextCodes(const FontMemory* memory, FontHandle font) { ParsedText parsed{"AVA", 0, font}; FontTextSource source{}; @@ -96,6 +115,73 @@ static void CheckTextCodes(const FontMemory* memory, FontHandle font) { std::uint32_t count = 0; FontTextCharacter* characters = sceFontStringRefersTextCharacters(string, &count); Require(characters != nullptr && count == 3); + Require(sceFontStringGetWritingForm(string) == 0x12); + FontString rtlString = nullptr; + Require(sceFontTextSourceSetWritingForm(&source, 0x11) == SCE_FONT_OK && sceFontCreateString(memory, &source, nullptr, &rtlString) == SCE_FONT_OK); + Require(sceFontStringGetWritingForm(rtlString) == 0x11); + Require(sceFontDestroyString(&rtlString) == SCE_FONT_OK); + Require(sceFontTextSourceSetWritingForm(&source, 0x12) == SCE_FONT_OK && sceFontCreateString(memory, &source, nullptr, &rtlString) == SCE_FONT_OK); + Require(sceFontStringGetWritingForm(rtlString) == 0x12); + Require(sceFontDestroyString(&rtlString) == SCE_FONT_OK); + Require(sceFontTextSourceSetWritingForm(&source, 0x10) == SCE_FONT_OK); + + const std::uint32_t latinCodes[]{'A', 0x00E9, 0x4E2D}; + const std::uint32_t greekCodes[]{0x03B1}; + const std::uint32_t digitsCodes[]{'1', '2', ' '}; + const std::uint32_t hebrewCodes[]{0x05E9, 0x05DC}; + const std::uint32_t arabicCodes[]{0x0627, 0x0644}; + const std::uint32_t syriacCodes[]{0x0710}; + const std::uint32_t thaanaCodes[]{0x0780}; + const std::uint32_t nkoCodes[]{0x07CA}; + const std::uint32_t hebrewLetterCodes[]{0xFB1D}; + const std::uint32_t arabicFormCodes[]{0xFE70}; + const std::uint32_t digitHebrewCodes[]{'1', ' ', 0x05D0}; + const std::uint32_t hebrewLatinCodes[]{0x05D0, 'A'}; + const std::uint32_t latinHebrewCodes[]{'A', 0x05D0}; + const std::uint32_t rlmLatinCodes[]{0x200F, 'A'}; + const std::uint32_t lrmHebrewCodes[]{0x200E, 0x05D0}; + const std::uint32_t embeddingCodes[]{0x202B, 'A'}; + const std::uint32_t overrideCodes[]{0x202E, 'A'}; + const std::uint32_t isolatedHebrewCodes[]{0x2067, 0x05D0, 0x2069, 'A'}; + const std::uint32_t isolatedLatinCodes[]{0x2066, 'A', 0x2069, 0x05D0}; + const std::uint32_t openIsolateCodes[]{0x2067, 0x05D0}; + const struct { const std::uint32_t* codes; std::size_t count; std::int32_t sourceForm; int expected; } forms[]{ + {latinCodes, 3, 0x10, 0x12}, + {greekCodes, 1, 0x10, 0x12}, + {digitsCodes, 3, 0x10, 0x12}, + {nullptr, 0, 0x10, 0x12}, + {hebrewCodes, 2, 0x10, 0x11}, + {arabicCodes, 2, 0x10, 0x11}, + {syriacCodes, 1, 0x10, 0x11}, + {thaanaCodes, 1, 0x10, 0x11}, + {nkoCodes, 1, 0x10, 0x11}, + {hebrewLetterCodes, 1, 0x10, 0x11}, + {arabicFormCodes, 1, 0x10, 0x11}, + {digitHebrewCodes, 3, 0x10, 0x11}, + {hebrewLatinCodes, 2, 0x10, 0x11}, + {latinHebrewCodes, 2, 0x10, 0x12}, + {rlmLatinCodes, 2, 0x10, 0x11}, + {lrmHebrewCodes, 2, 0x10, 0x12}, + {embeddingCodes, 2, 0x10, 0x12}, + {overrideCodes, 2, 0x10, 0x12}, + {isolatedHebrewCodes, 4, 0x10, 0x12}, + {isolatedLatinCodes, 4, 0x10, 0x11}, + {openIsolateCodes, 2, 0x10, 0x12}, + {hebrewCodes, 2, 0x11, 0x11}, + {hebrewCodes, 2, 0x12, 0x12}, + {latinCodes, 3, 0x11, 0x11}, + {latinCodes, 3, 0x12, 0x12}, + }; + for (const auto& form : forms) { + ParsedCodes parsedCodes{form.codes, form.count, 0, font}; + FontTextSource codeSource{}; + FontString codeString = nullptr; + Require(sceFontTextSourceInit(&codeSource, parsed.text, 3, ParseCodes, &parsedCodes) == SCE_FONT_OK); + Require(sceFontTextSourceSetWritingForm(&codeSource, form.sourceForm) == SCE_FONT_OK); + Require(sceFontCreateString(memory, &codeSource, nullptr, &codeString) == SCE_FONT_OK && codeString != nullptr); + Require(sceFontStringGetWritingForm(codeString) == form.expected); + Require(sceFontDestroyString(&codeString) == SCE_FONT_OK); + } FontTextCodes codes; std::memset(&codes, 0xa5, sizeof(codes)); @@ -585,11 +671,31 @@ int main() { Require(step != nullptr); Require(sceFontWritingRefersRenderStepCharacter(&writing, &out.letter) == characters && out.letter != nullptr && out.guard == guard); Require(out.letter->x == step->x && out.letter->advanceX == step->advanceX && out.letter->Components.textsIndex == 0 && out.letter->Components.characterTextCount == 1); + const FontWritingLetterStep* retainedLetter = out.letter; + const FontWritingLetterStep retainedValue = *retainedLetter; + Require(sceFontWritingRefersRenderStepCharacter(&writing, &out.letter) == nullptr && out.letter == nullptr && out.guard == guard); + Require(std::memcmp(retainedLetter, &retainedValue, sizeof(retainedValue)) == 0); + Require(sceFontWritingRefersRenderStepCharacter(&writing, nullptr) == nullptr); step = sceFontWritingRefersRenderStep(&writing); Require(step != nullptr); Require(sceFontWritingRefersRenderStepCharacter(&writing, &out.letter) == characters + 1 && out.guard == guard); Require(out.letter->x == step->x && out.letter->Components.textsIndex == 1 && out.letter->Components.glyphsIndex == 1); - Require(sceFontWritingRefersRenderStepCharacter(&writing, nullptr) == characters + 1); + Require(out.letter == retainedLetter); + Require(sceFontWritingRefersRenderStepCharacter(&writing, nullptr) == nullptr); + out.letter = retainedLetter; + Require(sceFontWritingRefersRenderStepCharacter(&writing, &out.letter) == nullptr && out.letter == nullptr && out.guard == guard); + Require(sceFontWritingRefersRenderStep(&writing) == nullptr); + Require(sceFontWritingRefersRenderStepCharacter(&writing, &out.letter) == nullptr && out.letter == nullptr); + Require(sceFontWritingInit(&writing, string, characters) == SCE_FONT_OK); + Require(sceFontWritingRefersRenderStepCharacter(&writing, &out.letter) == nullptr && out.letter == nullptr); + Require(sceFontWritingRefersRenderStep(&writing) != nullptr); + Require(sceFontWritingRefersRenderStepCharacter(&writing, nullptr) == characters); + Require(sceFontWritingRefersRenderStepCharacter(&writing, &out.letter) == nullptr && out.letter == nullptr); + Require(sceFontWritingInit(&writing, string, characters) == SCE_FONT_OK); + Require(sceFontWritingRefersRenderStep(&writing) != nullptr); + Require(sceFontWritingRefersRenderStep(&writing) != nullptr); + Require(sceFontWritingRefersRenderStepCharacter(&writing, &out.letter) == characters + 1 && out.letter == retainedLetter && out.guard == guard); + Require(sceFontWritingRefersRenderStepCharacter(&writing, &out.letter) == nullptr && out.letter == nullptr); Require(sceFontDestroyString(&string) == SCE_FONT_OK); CheckTextCodes(&memory, font); Require(sceFontCloseFont(font) == SCE_FONT_OK); diff --git a/core/libs/tests/GuestFontGraphics.cpp b/core/libs/tests/GuestFontGraphics.cpp new file mode 100644 index 0000000000..3093955bc9 --- /dev/null +++ b/core/libs/tests/GuestFontGraphics.cpp @@ -0,0 +1,214 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceFont/libSceFont.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr int InvalidParameter = static_cast(0x80460002); +constexpr int InvalidFill = static_cast(0x80460086); +constexpr int InvalidRegion = static_cast(0x80460087); +constexpr int InvalidFillPlot = static_cast(0x80460088); +constexpr int InvalidFillRates = static_cast(0x80460089); +constexpr float NaN = std::numeric_limits::quiet_NaN(); + +int g_lastError = 0; +int g_errorCount = 0; + +void APS5_VABI RecordError(int error) { + g_lastError = error; + ++g_errorCount; +} + +void Require(bool value) { if (!value) std::abort(); } + +template +void Fill(T& object, std::uint8_t value) { + std::memset(&object, value, sizeof(object)); +} + +template +bool AllBytes(const T& object, std::size_t from, std::size_t to, std::uint8_t value) { + const auto* bytes = reinterpret_cast(&object); + for (std::size_t i = from; i < to; ++i) if (bytes[i] != value) return false; + return true; +} + +int g_expectedErrors = 0; + +void RequireError(int result, int error) { + Require(result == error); + Require(g_lastError == error); + g_lastError = 0; + ++g_expectedErrors; +} + +const FontGraphicsFill* Sentinel(std::intptr_t value) { + return reinterpret_cast(value); +} + +void Regions() { + FontGraphicsRegion region; + Fill(region, 0xa5); + Require(sceFontGraphicsRegionInit(®ion, 1.0f, 2.0f, 3.0f, 4.0f) == 0); + Require(region.x == 1.0f && region.y == 2.0f && region.width == 3.0f && region.height == 4.0f); + Require(region.flags == 0xf && region.tag == 0xf60 && region.outerRadius == 0.0f && region.innerRadius == 0.0f); + Require(sceFontGraphicsRegionInit(®ion, 0.0f, 0.0f, 0.0f, 0.0f) == 0); + Require(sceFontGraphicsRegionInit(®ion, 0.0f, 0.0f, NaN, NaN) == 0); + Fill(region, 0xa5); + RequireError(sceFontGraphicsRegionInit(®ion, 0.0f, 0.0f, -1.0f, 1.0f), InvalidParameter); + RequireError(sceFontGraphicsRegionInit(®ion, 0.0f, 0.0f, 1.0f, -1.0f), InvalidParameter); + RequireError(sceFontGraphicsRegionInit(nullptr, 0.0f, 0.0f, 1.0f, 1.0f), InvalidParameter); + Require(AllBytes(region, 0, sizeof(region), 0xa5)); + + Require(sceFontGraphicsRegionInitCircular(®ion, 10.0f, 20.0f, 4.0f, 1.0f) == 0); + Require(region.x == 6.0f && region.y == 16.0f && region.width == 8.0f && region.height == 8.0f); + Require(region.flags == 0xff && region.tag == 0xf60 && region.outerRadius == 4.0f && region.innerRadius == 1.0f); + Require(sceFontGraphicsRegionInitCircular(®ion, 0.0f, 0.0f, 2.0f, 2.0f) == 0); + RequireError(sceFontGraphicsRegionInitCircular(®ion, 0.0f, 0.0f, 1.0f, 2.0f), InvalidParameter); + RequireError(sceFontGraphicsRegionInitCircular(®ion, 0.0f, 0.0f, 0.0f, 0.0f), InvalidParameter); + RequireError(sceFontGraphicsRegionInitCircular(®ion, 0.0f, 0.0f, 1.0f, 0.0f), InvalidParameter); + RequireError(sceFontGraphicsRegionInitCircular(nullptr, 0.0f, 0.0f, 2.0f, 1.0f), InvalidParameter); + + Require(sceFontGraphicsRegionInitRoundish(®ion, 1.0f, 2.0f, 10.0f, 8.0f, 4.0f, 2.0f, 0x5) == 0); + Require(region.width == 10.0f && region.height == 8.0f && region.flags == 0x5 && region.tag == 0xf60); + Require(region.outerRadius == 4.0f && region.innerRadius == 2.0f); + Require(sceFontGraphicsRegionInitRoundish(®ion, 0.0f, 0.0f, 10.0f, 8.0f, 0.0f, 9.0f, 0xff) == 0); + RequireError(sceFontGraphicsRegionInitRoundish(®ion, 0.0f, 0.0f, 10.0f, 8.0f, 4.5f, 0.0f, 0), InvalidParameter); + RequireError(sceFontGraphicsRegionInitRoundish(®ion, 0.0f, 0.0f, 10.0f, 8.0f, 2.0f, 3.0f, 0), InvalidParameter); + RequireError(sceFontGraphicsRegionInitRoundish(®ion, 0.0f, 0.0f, 10.0f, 8.0f, 2.0f, 1.0f, 0x100), InvalidParameter); + RequireError(sceFontGraphicsRegionInitRoundish(®ion, 0.0f, 0.0f, 0.0f, 8.0f, 0.0f, 0.0f, 0), InvalidParameter); + RequireError(sceFontGraphicsRegionInitRoundish(®ion, 0.0f, 0.0f, 10.0f, 8.0f, -1.0f, 0.0f, 0), InvalidParameter); +} + +void FillMethods() { + FontGraphicsFillMethod method; + Fill(method, 0xa5); + Require(sceFontGraphicsFillMethodInit(&method, 0x50, 12) == 0); + Require(method.mode == 0x50 && method.type == 12 && method.flags == 0xf && method.tag == 0xf01b); + Require(AllBytes(method, 16, 32, 0)); + for (std::int32_t mode : {0x00, 0x10, 0x20, 0x30, 0x40}) Require(sceFontGraphicsFillMethodInit(&method, mode, 1) == 0); + for (std::int32_t type = 1; type <= 7; ++type) Require(sceFontGraphicsFillMethodInit(&method, 0, type) == 0); + for (std::int32_t mode : {0x01, 0x08, 0x60, -0x10}) RequireError(sceFontGraphicsFillMethodInit(&method, mode, 1), InvalidParameter); + for (std::int32_t type : {0, 8, 11, 13, -1}) RequireError(sceFontGraphicsFillMethodInit(&method, 0, type), InvalidParameter); + RequireError(sceFontGraphicsFillMethodInit(nullptr, 0, 1), InvalidParameter); +} + +void FillPlots() { + FontGraphicsRegion region; + Require(sceFontGraphicsRegionInitRoundish(®ion, 1.0f, 2.0f, 10.0f, 8.0f, 4.0f, 2.0f, 0x5) == 0); + FontGraphicsFillPlot plot; + Fill(plot, 0xa5); + Require(sceFontGraphicsFillPlotInit(&plot, ®ion) == 0); + Require(plot.mapping[0] == 1.0f && plot.mapping[1] == 2.0f && plot.mapping[2] == 11.0f && plot.mapping[3] == 10.0f); + Require(plot.region == ®ion && plot.tag == 0xf01c && plot.reserved26 == 0 && plot.layout == 0xf77 && plot.flags == 1); + Require(AllBytes(plot, 31, 64, 0)); + + Fill(plot, 0xa5); + Require(sceFontGraphicsFillPlotInit(&plot, nullptr) == 0); + Require(plot.region == nullptr && plot.tag == 0xf01c && plot.layout == 0 && plot.flags == 2); + Require(AllBytes(plot, 0, 16, 0)); + const auto* whole = reinterpret_cast(std::intptr_t{-1}); + Require(sceFontGraphicsFillPlotInit(&plot, whole) == 0); + Require(plot.region == whole && plot.layout == 0xf77 && plot.flags == 6); + + FontGraphicsRegion bad = region; + bad.tag = 0; + Fill(plot, 0xa5); + RequireError(sceFontGraphicsFillPlotInit(&plot, &bad), InvalidRegion); + Require(plot.tag == 0 && AllBytes(plot, 0, 24, 0xa5) && AllBytes(plot, 26, 64, 0xa5)); + bad = region; + bad.outerRadius = NaN; + RequireError(sceFontGraphicsFillPlotInit(&plot, &bad), InvalidRegion); + bad = region; + bad.outerRadius = 4.5f; + RequireError(sceFontGraphicsFillPlotInit(&plot, &bad), InvalidRegion); + bad = region; + bad.outerRadius = 0.0f; + bad.innerRadius = 4.0f; + Require(sceFontGraphicsFillPlotInit(&plot, &bad) == 0); + bad.innerRadius = 4.5f; + RequireError(sceFontGraphicsFillPlotInit(&plot, &bad), InvalidRegion); + RequireError(sceFontGraphicsFillPlotInit(nullptr, ®ion), InvalidFillPlot); + + Require(sceFontGraphicsFillPlotInit(&plot, ®ion) == 0); + Require(sceFontGraphicsFillPlotSetLayout(&plot, 0x400, 0x24) == 0); + Require(plot.layout == 0x424 && plot.flags == 9); + Require(sceFontGraphicsFillPlotSetLayout(&plot, 0, 0) == 0); + Require(plot.layout == 0 && plot.flags == 8); + Require(sceFontGraphicsFillPlotSetLayout(&plot, 0x170, 0x1100) == 0); + Require(plot.layout == 0x1170); + RequireError(sceFontGraphicsFillPlotSetLayout(&plot, 0x171, 0), InvalidParameter); + RequireError(sceFontGraphicsFillPlotSetLayout(&plot, 0, 0x3), InvalidParameter); + RequireError(sceFontGraphicsFillPlotSetLayout(&plot, 0, 0x50), InvalidParameter); + Require(sceFontGraphicsFillPlotSetMapping(&plot, 5.0f, 6.0f, 7.0f, NaN) == 0); + Require(plot.mapping[0] == 5.0f && plot.mapping[2] == 7.0f && std::isnan(plot.mapping[3]) && plot.flags == 9); + + FontGraphicsFillPlot untagged{}; + RequireError(sceFontGraphicsFillPlotSetLayout(&untagged, 0, 0), InvalidFillPlot); + RequireError(sceFontGraphicsFillPlotSetMapping(&untagged, 0.0f, 0.0f, 0.0f, 0.0f), InvalidFillPlot); +} + +void FillRates() { + FontGraphicsFillRates rates; + Fill(rates, 0xa5); + std::array fill{0x30}; + const auto* fillPointer = reinterpret_cast(fill.data()); + Require(sceFontGraphicsFillRatesInit(&rates, fillPointer, 9) == 0); + Require(rates.fill == fillPointer && rates.tag == 0xf01a && rates.effect == 9 && rates.layout == 0xf77 && rates.flags == 2); + Require(AllBytes(rates, 0, 16, 0) && AllBytes(rates, 31, 64, 0)); + for (std::intptr_t value : {-1, 0, 1, 2}) Require(sceFontGraphicsFillRatesInit(&rates, Sentinel(value), 0) == 0); + for (std::uint32_t effect : {0u, 1u, 2u, 3u, 4u, 5u, 8u, 9u, 16u}) { + Require(sceFontGraphicsFillRatesInit(&rates, nullptr, effect) == 0); + Require(rates.effect == effect); + } + + Fill(rates, 0xa5); + RequireError(sceFontGraphicsFillRatesInit(&rates, nullptr, 6), InvalidParameter); + Require(AllBytes(rates, 0, sizeof(rates), 0)); + fill[0] = 0x31; + RequireError(sceFontGraphicsFillRatesInit(&rates, fillPointer, 0), InvalidFill); + fill[0] = 0x60; + RequireError(sceFontGraphicsFillRatesInit(&rates, fillPointer, 0), InvalidFill); + RequireError(sceFontGraphicsFillRatesInit(nullptr, nullptr, 0), InvalidParameter); + + fill[0] = 0x10; + Require(sceFontGraphicsFillRatesInit(&rates, nullptr, 0) == 0); + Require(sceFontGraphicsFillRatesSetFillEffect(&rates, fillPointer, 16) == 0); + Require(rates.fill == fillPointer && rates.effect == 16 && rates.flags == 2 && rates.layout == 0xf77); + RequireError(sceFontGraphicsFillRatesSetFillEffect(&rates, nullptr, 17), InvalidParameter); + Require(rates.fill == fillPointer && rates.effect == 16); + + Require(sceFontGraphicsFillRatesSetLayout(&rates, 0xf77, 0x44) == 0); + Require(rates.layout == 0 && rates.flags == 3); + Require(sceFontGraphicsFillRatesSetLayout(&rates, 0, 0) == 0); + Require(rates.layout == 0 && rates.flags == 2); + RequireError(sceFontGraphicsFillRatesSetLayout(&rates, 0x801, 0), InvalidParameter); + Require(sceFontGraphicsFillRatesSetMapping(&rates, 1.0f, 2.0f, 3.0f, 4.0f) == 0); + Require(rates.mapping[0] == 1.0f && rates.mapping[3] == 4.0f && rates.flags == 1); + + FontGraphicsFillRates untagged{}; + RequireError(sceFontGraphicsFillRatesSetFillEffect(&untagged, nullptr, 0), InvalidFillRates); + RequireError(sceFontGraphicsFillRatesSetLayout(&untagged, 0, 0), InvalidFillRates); + RequireError(sceFontGraphicsFillRatesSetMapping(&untagged, 0.0f, 0.0f, 0.0f, 0.0f), InvalidFillRates); +} + +} + +int main() { + sceFontSetGraphicsErrorCallback(RecordError); + Regions(); + FillMethods(); + FillPlots(); + FillRates(); + Require(g_errorCount == g_expectedErrors); + sceFontSetGraphicsErrorCallback(nullptr); + const int before = g_errorCount; + FontGraphicsRegion region; + Require(sceFontGraphicsRegionInit(®ion, 0.0f, 0.0f, -1.0f, 0.0f) == InvalidParameter); + Require(g_errorCount == before); +} diff --git a/core/libs/tests/GuestFontMetrics.cpp b/core/libs/tests/GuestFontMetrics.cpp new file mode 100644 index 0000000000..24d5f2b1ca --- /dev/null +++ b/core/libs/tests/GuestFontMetrics.cpp @@ -0,0 +1,137 @@ +#include "prx/libSceFont/include/FontDriver.hpp" +#include "prx/libSceFont/libSceFont.hpp" +#include "prx/libSceFontFt/libSceFontFt.hpp" +#include "tests/FontFixture.hpp" +#include +#include +#include +#include +#include +#include +#include + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Font metrics check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +struct Expected { + std::uint32_t form; + float ascender; + float descender; + float lineGap; + float maxAdvance; + float verticalDescender; + float verticalAscender; +}; + +static FontMetrics Metrics(FontHandle font, std::uint32_t form) { + FontMetrics metrics{}; + std::memset(&metrics, 0x7F, sizeof(metrics)); + Require(sceFontGetFontMetrics(font, form, &metrics) == SCE_FONT_OK); + return metrics; +} + +static void RequireMetrics(FontHandle font, std::uint32_t form, const Expected& expected) { + const FontMetrics metrics = Metrics(font, form); + Require(metrics.form == expected.form); + Require(metrics.ascender == expected.ascender && metrics.descender == expected.descender && metrics.lineGap == expected.lineGap); + Require(metrics.maxAdvance == expected.maxAdvance); + Require(metrics.verticalDescender == expected.verticalDescender && metrics.verticalAscender == expected.verticalAscender); + Require(metrics.xMin == 0.0f && metrics.yMin == 0.0f && metrics.xMax == 0.0f && metrics.yMax == 0.0f); + Require(metrics.emWidth == 99.984375f && metrics.emHeight == 99.984375f); +} + +static void* APS5_VABI Allocate(void*, std::uint32_t size) { + return std::malloc(size); +} + +static void APS5_VABI Release(void*, void* pointer) { + std::free(pointer); +} + +using FontFixture::BuildFont; +using FontFixture::Words; + +static std::vector Os2Table() { + return Words({0, 500, 400, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0x20, 0x7E, 700, -300, 100, 900, 250}); +} + +static FontHandle Open(FontLibrary library, const std::vector& data) { + FontHandle font = nullptr; + Require(sceFontOpenFontMemory(library, data.data(), static_cast(data.size()), nullptr, &font) == SCE_FONT_OK && font != nullptr); + Require(sceFontSetScalePixel(font, 100.0f, 100.0f) == SCE_FONT_OK); + return font; +} + +static std::vector VerticalHeader(int major) { + return Words({major, 0, 880, -120, 60, 1000, 40, 0, 950, 0, 0, 0, 0, 0, 0, 0, 0, 1}); +} + +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, 4, 0x52) == SCE_FONT_OK); + + const auto plainData = BuildFont(1, {}); + FontHandle plain = Open(library, plainData); + RequireMetrics(plain, 1, {1, 80.0f, -20.0f, 0.0f, 50.0f, 0.0f, 0.0f}); + RequireMetrics(plain, 2, {1, 80.0f, -20.0f, 0.0f, 50.0f, 0.0f, 0.0f}); + RequireMetrics(plain, 3, {1, 80.0f, -20.0f, 0.0f, 50.0f, 0.0f, 0.0f}); + RequireMetrics(plain, 4, {4, 0.0f, 100.0f, 0.0f, 100.0f, -50.0f, 80.0f}); + FontMetrics untouched{}; + untouched.ascender = 5.0f; + untouched.descender = 6.0f; + untouched.lineGap = 7.0f; + Require(sceFontGetFontMetrics(plain, 7, &untouched) == SCE_FONT_OK); + Require(untouched.form == 7 && untouched.ascender == 5.0f && untouched.descender == 6.0f && untouched.lineGap == 7.0f); + Require(untouched.maxAdvance == 50.0f && untouched.verticalDescender == 0.0f && untouched.verticalAscender == 0.0f); + + const auto typoData = BuildFont(1, {{"OS/2", Os2Table()}, {"vhea", VerticalHeader(0x11)}, {"vmtx", Words({1000, 40})}}); + FontHandle typo = Open(library, typoData); + RequireMetrics(typo, 1, {1, 80.0f, -20.0f, 0.0f, 50.0f, 0.0f, 0.0f}); + RequireMetrics(typo, 2, {2, 70.0f, -30.0f, 10.0f, 50.0f, 0.0f, 0.0f}); + RequireMetrics(typo, 3, {3, 90.0f, -25.0f, 0.0f, 50.0f, 0.0f, 0.0f}); + RequireMetrics(typo, 4, {4, 4.0f, 95.0f, -93.984375f, 100.0f, -12.0f, 70.0f}); + Require(sceFontSetEffectWeight(typo, 1.03125f, 0.984375f, 0) == SCE_FONT_OK); + Require(sceFontSetEffectSlant(typo, 0.25f) == SCE_FONT_OK); + const FontMetrics styled = Metrics(typo, 2); + Require(styled.effectWeightX == 0.03125f && styled.effectWeightY == -0.015625f && styled.slantRatio == 0.25f); + Require(sceFontSetEffectWeight(typo, 1.5f, 0.5f, 0) == SCE_FONT_OK); + const FontMetrics clamped = Metrics(typo, 2); + Require(clamped.effectWeightX == 0.04f && clamped.effectWeightY == -0.04f); + + const auto versionOneData = BuildFont(1, {{"vhea", VerticalHeader(1)}, {"vmtx", Words({1000, 40})}}); + FontHandle versionOne = Open(library, versionOneData); + RequireMetrics(versionOne, 4, {4, 4.0f, 95.0f, 0.0f, 100.0f, -12.0f, 4.0f}); + + const auto os2OnlyData = BuildFont(1, {{"OS/2", Os2Table()}}); + FontHandle os2Only = Open(library, os2OnlyData); + RequireMetrics(os2Only, 4, {4, 70.0f, 70.0f, 0.0f, 100.0f, -50.0f, 70.0f}); + + FontMetrics cleared{}; + std::memset(&cleared, 0x7F, sizeof(cleared)); + Require(sceFontGetFontMetrics(nullptr, 1, &cleared) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(cleared.form == 0 && cleared.ascender == 0.0f && cleared.maxAdvance == 0.0f && cleared.emHeight == 0.0f); + Require(cleared.effectWeightX == 1.0f && cleared.effectWeightY == 1.0f && cleared.slantRatio == 0.0f); + std::uint64_t notAFont[32]{}; + std::memset(&cleared, 0x7F, sizeof(cleared)); + Require(sceFontGetFontMetrics(reinterpret_cast(notAFont), 1, &cleared) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(cleared.form == 0 && cleared.effectWeightX == 1.0f); + Require(sceFontGetFontMetrics(plain, 1, nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontGetFontMetrics(nullptr, 1, nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + + for (FontHandle font : {plain, typo, versionOne, os2Only}) Require(sceFontCloseFont(font) == SCE_FONT_OK); + Require(sceFontDestroyLibrary(&library) == SCE_FONT_OK); + Require(sceFontMemoryTerm(&memory) == SCE_FONT_OK); + std::puts("Font metrics tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestFontOutlineProfile.cpp b/core/libs/tests/GuestFontOutlineProfile.cpp new file mode 100644 index 0000000000..1c57782d45 --- /dev/null +++ b/core/libs/tests/GuestFontOutlineProfile.cpp @@ -0,0 +1,121 @@ +#include "prx/libSceFont/include/FontDriver.hpp" +#include "prx/libSceFont/libSceFont.hpp" +#include "prx/libSceFontFt/libSceFontFt.hpp" +#include "tests/FontFixture.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Font outline profile check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static void* APS5_VABI Allocate(void*, std::uint32_t size) { + return std::malloc(size); +} + +static void APS5_VABI Release(void*, void* pointer) { + std::free(pointer); +} + +static void PatchMaxp(std::vector& font, std::initializer_list words) { + const int tableCount = (font[4] << 8) | font[5]; + for (int i = 0; i < tableCount; ++i) { + const std::size_t entry = 12 + 16 * static_cast(i); + if (std::memcmp(font.data() + entry, "maxp", 4) != 0) continue; + std::size_t offset = (static_cast(font[entry + 8]) << 24) | (static_cast(font[entry + 9]) << 16) | + (static_cast(font[entry + 10]) << 8) | font[entry + 11]; + for (const int word : words) { + if (word >= 0) { + font[offset] = static_cast((word >> 8) & 0xFF); + font[offset + 1] = static_cast(word & 0xFF); + } + offset += 2; + } + return; + } + Require(false); +} + +static FontHandle Open(FontLibrary library, const std::vector& data) { + FontHandle font = nullptr; + Require(sceFontOpenFontMemory(library, data.data(), static_cast(data.size()), nullptr, &font) == SCE_FONT_OK && font != nullptr); + return font; +} + +static void Fill(FontOutlineProfile& profile) { + std::memset(&profile, 0x7F, sizeof(profile)); +} + +static bool Zero(const FontOutlineProfile& profile) { + return profile.maxContours == 0 && profile.maxPoints == 0 && profile.maxCompositeContours == 0 && profile.maxCompositePoints == 0; +} + +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, 4, 0x52) == SCE_FONT_OK); + + auto profiledData = FontFixture::BuildFont(1, {}); + PatchMaxp(profiledData, {-1, -1, -1, 120, 7, 300, 11}); + FontHandle profiled = Open(library, profiledData); + FontOutlineProfile profile{}; + Fill(profile); + Require(sceFontGetFontGlyphsOutlineProfile(profiled, &profile) == SCE_FONT_OK); + Require(profile.maxContours == 7 && profile.maxPoints == 120 && profile.maxCompositeContours == 11 && profile.maxCompositePoints == 300); + + const auto plainData = FontFixture::BuildFont(1, {}); + FontHandle plain = Open(library, plainData); + Fill(profile); + Require(sceFontGetFontGlyphsOutlineProfile(plain, &profile) == SCE_FONT_OK && Zero(profile)); + + auto shortData = FontFixture::BuildFont(1, {}); + PatchMaxp(shortData, {0, 0x5000}); + FontHandle shortMaxp = Open(library, shortData); + Fill(profile); + Require(sceFontGetFontGlyphsOutlineProfile(shortMaxp, &profile) == SCE_FONT_ERROR_NO_SUPPORT_FUNCTION && Zero(profile)); + + std::array record{}; + auto* nativeFont = reinterpret_cast(profiled); + nativeFont->open_info.fontset_record = record.data(); + Require(sceFontGetFontGlyphsOutlineProfile(profiled, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + nativeFont->open_info.fontset_record = nullptr; + Fill(profile); + Require(sceFontGetFontGlyphsOutlineProfile(profiled, &profile) == SCE_FONT_OK && profile.maxPoints == 120); + + Fill(profile); + Require(sceFontGetFontGlyphsOutlineProfile(nullptr, &profile) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && Zero(profile)); + std::uint64_t notAFont[32]{}; + Fill(profile); + Require(sceFontGetFontGlyphsOutlineProfile(reinterpret_cast(notAFont), &profile) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && Zero(profile)); + Require(sceFontGetFontGlyphsOutlineProfile(plain, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontGetFontGlyphsOutlineProfile(nullptr, nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontGetFontGlyphsOutlineProfile(reinterpret_cast(notAFont), nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + auto* plainFont = reinterpret_cast(plain); + const FontLibrary owner = plainFont->library; + plainFont->library = nullptr; + Fill(profile); + Require(sceFontGetFontGlyphsOutlineProfile(plain, &profile) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && Zero(profile)); + Require(sceFontGetFontGlyphsOutlineProfile(plain, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + plainFont->library = owner; + Fill(profile); + Require(sceFontGetFontGlyphsOutlineProfile(plain, &profile) == SCE_FONT_OK && Zero(profile)); + + for (FontHandle font : {profiled, plain, shortMaxp}) Require(sceFontCloseFont(font) == SCE_FONT_OK); + Require(sceFontDestroyLibrary(&library) == SCE_FONT_OK); + Require(sceFontMemoryTerm(&memory) == SCE_FONT_OK); + std::puts("Font outline profile tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestFontStyleInformation.cpp b/core/libs/tests/GuestFontStyleInformation.cpp new file mode 100644 index 0000000000..21443be90b --- /dev/null +++ b/core/libs/tests/GuestFontStyleInformation.cpp @@ -0,0 +1,158 @@ +#include "prx/libSceFont/include/FontDriver.hpp" +#include "prx/libSceFont/libSceFont.hpp" +#include "prx/libSceFontFt/libSceFontFt.hpp" +#include "tests/FontFixture.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Font style information check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +using FontFixture::BuildFont; +using FontFixture::Words; + +static void* APS5_VABI Allocate(void*, std::uint32_t size) { + return std::malloc(size); +} + +static void APS5_VABI Release(void*, void* pointer) { + std::free(pointer); +} + +static std::vector NameTable(std::string_view family, std::string_view style) { + const int familyBytes = static_cast(family.size() * 2); + const int styleBytes = static_cast(style.size() * 2); + std::vector table = Words({0, 2, 30, 3, 1, 0x409, 1, familyBytes, 0, 3, 1, 0x409, 2, styleBytes, familyBytes}); + for (const char c : family) FontFixture::Put16(table, c); + for (const char c : style) FontFixture::Put16(table, c); + return table; +} + +static std::vector Os2Table(int weight, int width) { + return Words({0, 500, weight, width, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0x20, 0x7E, 700, -300, 100, 900, 250}); +} + +static void PatchHead(std::vector& font, int macStyle, int direction) { + const int tableCount = (font[4] << 8) | font[5]; + for (int i = 0; i < tableCount; ++i) { + const std::size_t entry = 12 + 16 * static_cast(i); + if (std::memcmp(font.data() + entry, "head", 4) != 0) continue; + const std::size_t offset = (static_cast(font[entry + 8]) << 24) | (static_cast(font[entry + 9]) << 16) | + (static_cast(font[entry + 10]) << 8) | font[entry + 11]; + font[offset + 44] = static_cast((macStyle >> 8) & 0xFF); + font[offset + 45] = static_cast(macStyle & 0xFF); + font[offset + 48] = static_cast((direction >> 8) & 0xFF); + font[offset + 49] = static_cast(direction & 0xFF); + return; + } + Require(false); +} + +static FontHandle Open(FontLibrary library, const std::vector& data) { + FontHandle font = nullptr; + Require(sceFontOpenFontMemory(library, data.data(), static_cast(data.size()), nullptr, &font) == SCE_FONT_OK && font != nullptr); + return font; +} + +static FontStyleInformation Information(FontHandle font) { + FontStyleInformation information{}; + std::memset(&information, 0x7F, sizeof(information)); + Require(sceFontGetFontStyleInformation(font, &information) == SCE_FONT_OK); + return information; +} + +static bool Cleared(const FontStyleInformation& information) { + return information.familyName && information.familyName[0] == '\0' && information.styleName && information.styleName[0] == '\0' && + information.styleFlags == 0 && information.weight == 0 && information.width == 0 && information.layoutFlags == 0; +} + +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, 4, 0x52) == SCE_FONT_OK); + + auto namedData = BuildFont(1, {{"name", NameTable("Tst", "Bd")}}); + PatchHead(namedData, 3, -2); + FontHandle named = Open(library, namedData); + const FontStyleInformation namedInfo = Information(named); + Require(namedInfo.familyName && std::string(namedInfo.familyName) == "Tst"); + Require(namedInfo.styleName && std::string(namedInfo.styleName) == "Bd"); + Require(namedInfo.styleFlags == 3 && namedInfo.weight == 0 && namedInfo.width == 0 && namedInfo.layoutFlags == 2); + + const auto os2Data = BuildFont(1, {{"name", NameTable("Tst", "Rg")}, {"OS/2", Os2Table(400, 5)}, + {"vhea", Words({0x11, 0, 880, -120, 60, 1000, 40, 0, 950, 0, 0, 0, 0, 0, 0, 0, 0, 1})}, {"vmtx", Words({1000, 40})}}); + FontHandle os2 = Open(library, os2Data); + const FontStyleInformation os2Info = Information(os2); + Require(os2Info.styleFlags == 0 && os2Info.weight == 400 && os2Info.width == 5 && os2Info.layoutFlags == 0x81); + + const auto oddData = BuildFont(1, {{"name", NameTable("Tst", "Rg")}, {"OS/2", Os2Table(450, 12)}}); + FontHandle odd = Open(library, oddData); + const FontStyleInformation oddInfo = Information(odd); + Require(oddInfo.weight == 0 && oddInfo.width == 0 && oddInfo.layoutFlags == 1); + + for (const int weight : {100, 900}) { + const auto data = BuildFont(1, {{"name", NameTable("Tst", "Rg")}, {"OS/2", Os2Table(weight, 9)}}); + FontHandle font = Open(library, data); + const FontStyleInformation info = Information(font); + Require(info.weight == weight && info.width == 9); + Require(sceFontCloseFont(font) == SCE_FONT_OK); + } + for (const int weight : {0, 1000}) { + const auto data = BuildFont(1, {{"name", NameTable("Tst", "Rg")}, {"OS/2", Os2Table(weight, 10)}}); + FontHandle font = Open(library, data); + const FontStyleInformation info = Information(font); + Require(info.weight == 0 && info.width == 0); + Require(sceFontCloseFont(font) == SCE_FONT_OK); + } + + static const char recordFamily[] = "Rf"; + static const char recordStyle[] = "Rs"; + std::array record{}; + record[4] = std::byte{1}; + record[5] = std::byte{7}; + record[6] = std::byte{3}; + record[7] = std::byte{0x81}; + const char* familyPointer = recordFamily; + const char* stylePointer = recordStyle; + std::memcpy(record.data() + 8, &familyPointer, sizeof(familyPointer)); + std::memcpy(record.data() + 16, &stylePointer, sizeof(stylePointer)); + auto* nativeFont = reinterpret_cast(os2); + nativeFont->open_info.fontset_record = record.data(); + const FontStyleInformation recordInfo = Information(os2); + nativeFont->open_info.fontset_record = nullptr; + Require(recordInfo.familyName == recordFamily && recordInfo.styleName == recordStyle); + Require(recordInfo.styleFlags == 1 && recordInfo.weight == 700 && recordInfo.width == 3 && recordInfo.layoutFlags == 0x81); + + FontStyleInformation cleared{}; + std::memset(&cleared, 0x7F, sizeof(cleared)); + Require(sceFontGetFontStyleInformation(nullptr, &cleared) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && Cleared(cleared)); + std::uint64_t notAFont[32]{}; + std::memset(&cleared, 0x7F, sizeof(cleared)); + Require(sceFontGetFontStyleInformation(reinterpret_cast(notAFont), &cleared) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && Cleared(cleared)); + Require(sceFontGetFontStyleInformation(named, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontGetFontStyleInformation(nullptr, nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + + for (FontHandle font : {named, os2, odd}) Require(sceFontCloseFont(font) == SCE_FONT_OK); + Require(sceFontDestroyLibrary(&library) == SCE_FONT_OK); + Require(sceFontMemoryTerm(&memory) == SCE_FONT_OK); + std::puts("Font style information tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestFontWords.cpp b/core/libs/tests/GuestFontWords.cpp new file mode 100644 index 0000000000..6a23430a7c --- /dev/null +++ b/core/libs/tests/GuestFontWords.cpp @@ -0,0 +1,314 @@ +#include "prx/libSceFont/libSceFont.hpp" +#include +#include +#include +#include +#include +#include + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Font words check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +struct AllocationState { + std::size_t live = 0; + bool fail = false; +}; + +static void* APS5_VABI Allocate(void* object, std::uint32_t size) { + auto& state = *static_cast(object); + if (state.fail) return nullptr; + void* allocation = std::malloc(size == 0 ? 1 : size); + if (allocation) ++state.live; + return allocation; +} + +static void APS5_VABI Deallocate(void* object, void* allocation) { + auto& state = *static_cast(object); + Require(allocation && state.live > 0); + --state.live; + std::free(allocation); +} + +struct WordText { + std::vector codes; + std::size_t position = 0; + std::size_t parses = 0; + std::int32_t errorCode = 0; +}; + +static std::int32_t APS5_VABI ParseWords(FontTextSource* source, void** order, FontTextParseResult* result) { + auto& text = *static_cast(source->textObject); + ++text.parses; + if (text.errorCode != 0 && text.position == 1) { + result->Error.errorCode = text.errorCode; + return -1; + } + const std::uint32_t code = text.position < text.codes.size() ? text.codes[text.position] : 0; + *order = reinterpret_cast(text.position); + ++text.position; + if (code == 0 || code == ' ') { + result->Terminate.terminateCode = code; + return 0; + } + result->FontCode.code = code; + return 1; +} + +static int APS5_VABI RewindWords(FontTextSource* source) { + static_cast(source->textObject)->position = 0; + return SCE_FONT_OK; +} + +static WordText Text(const std::string& value) { + WordText text; + for (const char c : value) text.codes.push_back(static_cast(c)); + return text; +} + +struct Fixture { + AllocationState allocations; + FontMemoryInterface iface{Allocate, Deallocate, nullptr, nullptr, nullptr, nullptr}; + FontMemory memory{}; + + Fixture() { Require(sceFontMemoryInit(&memory, nullptr, 0, &iface, &allocations, nullptr, nullptr) == SCE_FONT_OK); } + + int Create(WordText& text, FontWords** words) { + FontTextSource source{}; + Require(sceFontTextSourceInit(&source, nullptr, 0, ParseWords, &text) == SCE_FONT_OK); + text.position = 0; + const int rc = sceFontCreateWords(&memory, &source, 0, words); + RewindWords(&source); + return rc; + } +}; + +struct Characters { + std::vector items; + + explicit Characters(const std::string& value, std::uint64_t flags = 0) : items(value.size()) { + for (std::size_t i = 0; i < value.size(); ++i) { + items[i].characterCode = static_cast(value[i]); + items[i].clusterSpan = 1; + items[i].flags = flags; + } + Link(); + } + + void Link() { + for (std::size_t i = 0; i < items.size(); ++i) { + items[i].prev = i == 0 ? nullptr : &items[i - 1]; + items[i].next = i + 1 < items.size() ? &items[i + 1] : nullptr; + } + } + + FontTextCharacter* operator[](std::size_t index) { return &items[index]; } +}; + +static FontTextCharacter* Find(FontWords* words, FontTextCharacter* first, FontTextCharacter** next = nullptr, FontTextCharacter* end = nullptr) { + FontTextCharacter* last = reinterpret_cast(1); + FontTextCharacter* following = reinterpret_cast(1); + Require(sceFontWordsFindWordCharacters(words, first, end, &last, &following) == SCE_FONT_OK); + if (next) *next = following; + return last; +} + +static void CreateValidation() { + Fixture fixture; + WordText text = Text("an"); + FontTextSource source{}; + Require(sceFontTextSourceInit(&source, nullptr, 0, ParseWords, &text) == SCE_FONT_OK); + FontWords* words = reinterpret_cast(1); + Require(sceFontCreateWords(nullptr, &source, 0, &words) == SCE_FONT_ERROR_INVALID_PARAMETER && words == nullptr); + FontMemory badMemory{}; + words = reinterpret_cast(1); + Require(sceFontCreateWords(&badMemory, &source, 0, &words) == SCE_FONT_ERROR_INVALID_MEMORY && words == nullptr); + Require(sceFontCreateWords(&fixture.memory, nullptr, 0, &words) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontCreateWords(&fixture.memory, &source, 0, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + FontTextSource unknown = source; + unknown.systemUse0 = 0; + Require(sceFontCreateWords(&fixture.memory, &unknown, 0, &words) == SCE_FONT_ERROR_INVALID_TEXT_SOURCE && words == nullptr); + Require(sceFontTextSourceRewind(&unknown) == SCE_FONT_ERROR_INVALID_TEXT_SOURCE); + Require(sceFontTextSourceSetDefaultFont(&unknown, nullptr) == SCE_FONT_ERROR_INVALID_TEXT_SOURCE); + Require(sceFontTextSourceSetWritingForm(&unknown, 0x10) == SCE_FONT_ERROR_INVALID_TEXT_SOURCE); + Require(sceFontTextSourceRewind(nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontTextSourceSetWritingForm(&source, 0x13) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(text.parses == 0); + Require(fixture.allocations.live == 0); +} + +static void CreateFailures() { + Fixture fixture; + FontWords* words = reinterpret_cast(1); + WordText unsorted = Text("be an"); + Require(fixture.Create(unsorted, &words) == SCE_FONT_ERROR_NO_SUPPORT_CODE && words == nullptr); + WordText sibling = Text("abc abb"); + Require(fixture.Create(sibling, &words) == SCE_FONT_ERROR_NO_SUPPORT_CODE && words == nullptr); + WordText prefixThenSmaller = Text("abc ab abb"); + Require(fixture.Create(prefixThenSmaller, &words) == SCE_FONT_ERROR_NO_SUPPORT_CODE && words == nullptr); + WordText negative; + negative.codes = {'a', 0x80000000u}; + Require(fixture.Create(negative, &words) == SCE_FONT_ERROR_NO_SUPPORT_CODE && words == nullptr); + WordText failing = Text("an"); + failing.errorCode = static_cast(0x80460099); + Require(fixture.Create(failing, &words) == static_cast(0x80460099) && words == nullptr); + failing = Text("an"); + failing.errorCode = 5; + Require(fixture.Create(failing, &words) == SCE_FONT_ERROR_INVALID_TEXT_SOURCE && words == nullptr); + WordText valid = Text("an"); + fixture.allocations.fail = true; + Require(fixture.Create(valid, &words) == SCE_FONT_ERROR_ALLOCATION_FAILED && words == nullptr); + fixture.allocations.fail = false; + Require(fixture.allocations.live == 0); +} + +static void CreateAndDestroy() { + Fixture fixture; + WordText text = Text("an and ant be"); + FontWords* words = nullptr; + Require(fixture.Create(text, &words) == SCE_FONT_OK && words != nullptr); + Require(fixture.allocations.live == 2); + FontWords* copy = words; + Require(sceFontDestroyWords(&words) == SCE_FONT_OK && words == nullptr); + Require(fixture.allocations.live == 0); + Require(sceFontDestroyWords(&words) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontDestroyWords(nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + std::uint64_t garbage[8]{}; + copy = reinterpret_cast(garbage); + Require(sceFontDestroyWords(©) == SCE_FONT_ERROR_INVALID_WORDS && copy != nullptr); + WordText empty; + Require(fixture.Create(empty, &words) == SCE_FONT_OK && words != nullptr); + Characters chars("an"); + Require(Find(words, chars[0]) == nullptr); + Require(sceFontDestroyWords(&words) == SCE_FONT_OK); + Require(fixture.allocations.live == 0); +} + +static void FindValidation() { + Fixture fixture; + WordText text = Text("an"); + FontWords* words = nullptr; + Require(fixture.Create(text, &words) == SCE_FONT_OK); + Characters chars("an"); + FontTextCharacter* last = chars[0]; + FontTextCharacter* next = chars[0]; + Require(sceFontWordsFindWordCharacters(nullptr, chars[0], nullptr, &last, &next) == SCE_FONT_ERROR_INVALID_PARAMETER && last == nullptr && next == nullptr); + next = chars[0]; + Require(sceFontWordsFindWordCharacters(words, nullptr, nullptr, &last, &next) == SCE_FONT_ERROR_INVALID_PARAMETER && next == nullptr); + Require(sceFontWordsFindWordCharacters(words, chars[0], nullptr, nullptr, &next) == SCE_FONT_ERROR_INVALID_PARAMETER); + std::uint64_t garbage[8]{}; + last = chars[0]; + Require(sceFontWordsFindWordCharacters(reinterpret_cast(garbage), chars[0], nullptr, &last, nullptr) == SCE_FONT_ERROR_INVALID_WORDS && last == nullptr); + Require(sceFontDestroyWords(&words) == SCE_FONT_OK); +} + +static void LongestMatch() { + Fixture fixture; + WordText text = Text("an and ant be"); + FontWords* words = nullptr; + Require(fixture.Create(text, &words) == SCE_FONT_OK); + Characters andy("andy"); + FontTextCharacter* next = nullptr; + Require(Find(words, andy[0], &next) == andy[2] && next == andy[3]); + Characters ant("ant"); + Require(Find(words, ant[0], &next) == ant[2] && next == nullptr); + Characters ab("ab"); + Require(Find(words, ab[0], &next) == nullptr && next == nullptr); + Characters ba("ba"); + Require(Find(words, ba[0]) == nullptr); + Characters band("and"); + Require(Find(words, band[0], &next, band[2]) == band[1] && next == band[2]); + Characters empty("be"); + Require(Find(words, empty[0], &next, empty[0]) == empty[1] && next == nullptr); + Characters joined("andy"); + joined.items[3].flags = std::uint64_t{1} << 43; + Require(Find(words, joined[0]) == joined[1]); + joined.items[3].flags = std::uint64_t{3} << 43; + Require(Find(words, joined[0]) == joined[2]); + Require(sceFontDestroyWords(&words) == SCE_FONT_OK); +} + +static void Clusters() { + Fixture fixture; + WordText text = Text("ab abc"); + FontWords* words = nullptr; + Require(fixture.Create(text, &words) == SCE_FONT_OK); + Characters split("abcd"); + split.items[2].clusterIndex = 1; + FontTextCharacter* next = nullptr; + Require(Find(words, split[0], &next) == split[1] && next == split[3]); + Characters marked("abxc"); + marked.items[2].synthetic = 1; + Require(Find(words, marked[0], &next) == marked[3] && next == nullptr); + marked.items[2].synthetic = -1; + marked.items[2].clusterKind = 1; + Require(Find(words, marked[0]) == marked[3]); + Characters lead("abxc"); + lead.items[2].synthetic = -1; + lead.items[2].clusterKind = 2; + lead.items[3].characterCode = 'z'; + Require(Find(words, lead[0]) == nullptr); + lead.items[2].clusterKind = 1; + Require(Find(words, lead[0], &next) == lead[1] && next == lead[3]); + Require(sceFontDestroyWords(&words) == SCE_FONT_OK); +} + +static void SegmentedRuns() { + Fixture fixture; + WordText text = Text("an ant be tea"); + FontWords* words = nullptr; + Require(fixture.Create(text, &words) == SCE_FONT_OK); + Characters following("antbe", 1); + Require(Find(words, following[0]) == following[2]); + Characters shorter("antea", 1); + Require(Find(words, shorter[0]) == shorter[1]); + Characters fallback("antx", 1); + Require(Find(words, fallback[0]) == fallback[2]); + Require(sceFontDestroyWords(&words) == SCE_FONT_OK); + + WordText overlapping = Text("ab bcd"); + Require(fixture.Create(overlapping, &words) == SCE_FONT_OK); + Characters overrun("abcd", 1); + overrun.items[0].flags = 0; + FontTextCharacter* next = overrun[0]; + Require(Find(words, overrun[0], &next) == nullptr && next == nullptr); + Characters contained("abcd", 1); + Require(Find(words, contained[0]) == contained[1]); + Require(sceFontDestroyWords(&words) == SCE_FONT_OK); + Require(fixture.allocations.live == 0); +} + +static void DepthLimit() { + Fixture fixture; + WordText text = Text(std::string(129, 'a') + " b"); + FontWords* words = nullptr; + Require(fixture.Create(text, &words) == SCE_FONT_OK); + Characters longWord(std::string(129, 'a')); + Require(Find(words, longWord[0]) == nullptr); + Characters later("b"); + Require(Find(words, later[0]) == nullptr); + Require(sceFontDestroyWords(&words) == SCE_FONT_OK); + WordText exact = Text(std::string(128, 'a') + " b"); + Require(fixture.Create(exact, &words) == SCE_FONT_OK); + Characters fullWord(std::string(128, 'a')); + Require(Find(words, fullWord[0]) == fullWord[127]); + Require(Find(words, later[0]) == later[0]); + Require(sceFontDestroyWords(&words) == SCE_FONT_OK); +} + +int main() { + CreateValidation(); + CreateFailures(); + CreateAndDestroy(); + FindValidation(); + LongestMatch(); + Clusters(); + SegmentedRuns(); + DepthLimit(); + std::puts("Font words tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestFopenErrors.cpp b/core/libs/tests/GuestFopenErrors.cpp index 7cfd9c96b6..8b2f1f0d81 100644 --- a/core/libs/tests/GuestFopenErrors.cpp +++ b/core/libs/tests/GuestFopenErrors.cpp @@ -7,7 +7,11 @@ #include #include #include +#ifdef _WIN32 +#include +#else #include +#endif namespace { @@ -22,6 +26,7 @@ void Require(bool value, int line) { constexpr int GuestEnoent = 2; constexpr int GuestEisdir = 21; constexpr int GuestEinval = 22; +constexpr int GuestEnotsup = 45; constexpr int GuestEloop = 62; constexpr int GuestEnametoolong = 63; @@ -32,7 +37,17 @@ std::filesystem::path EnterFreshRoot() { ("anyps5-fopen-errors-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); std::filesystem::create_directories(root / "folder"); std::filesystem::current_path(root); +#ifdef _WIN32 + if (CreateSymbolicLinkW(L"loop", L"loop", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) == 0) { + Check(GetLastError() == ERROR_PRIVILEGE_NOT_HELD); + std::fprintf(stderr, "fopen error checks skipped: creating symbolic links needs Developer Mode or administrator rights\n"); + std::filesystem::current_path(root.parent_path()); + std::filesystem::remove_all(root); + std::exit(77); + } +#else Check(::symlink("loop", "loop") == 0); +#endif { std::ofstream file("plain.txt"); file << "plain"; } return root; } @@ -88,6 +103,31 @@ void CheckedOpen() { Check(fopen_s_nid_postfix(&stream, "plain.txt", "r") == 0 && stream != nullptr && fclose_nid_postfix(stream) == 0); } +void CheckedReopen() { + FileStream* const sentinel = reinterpret_cast(1); + FileStream* result = sentinel; + FileStream* stream = fopen_nid_postfix("plain.txt", "r"); + Check(stream != nullptr); + Check(freopen_s_nid_postfix(nullptr, "plain.txt", "r", stream) == GuestEinval); + Check(freopen_s_nid_postfix(&result, nullptr, "r", stream) == GuestEnotsup && result == nullptr); + result = sentinel; + Check(freopen_s_nid_postfix(&result, "plain.txt", nullptr, stream) == GuestEinval && result == nullptr); + result = sentinel; + Check(freopen_s_nid_postfix(&result, "plain.txt", "r", nullptr) == GuestEinval && result == nullptr); + result = sentinel; + Check(freopen_s_nid_postfix(&result, "plain.txt", "z", stream) == GuestEinval && result == nullptr); + Check(freopen_s_nid_postfix(&result, "reopened.txt", "w", stream) == 0 && result == stream); + Check(fclose_nid_postfix(stream) == 0 && std::filesystem::exists("reopened.txt")); + stream = fopen_nid_postfix("plain.txt", "r"); + Check(stream != nullptr); + result = sentinel; + Check(freopen_s_nid_postfix(&result, "loop", "r", stream) == GuestEloop && result == nullptr); + stream = fopen_nid_postfix("plain.txt", "r"); + Check(stream != nullptr); + result = sentinel; + Check(freopen_s_nid_postfix(&result, "missing.txt", "r", stream) == GuestEnoent && result == nullptr); +} + } int main() { @@ -95,7 +135,11 @@ int main() { Open(); Reopen(); CheckedOpen(); + CheckedReopen(); std::error_code error; +#ifdef _WIN32 + DeleteFileW(L"loop"); +#endif std::filesystem::current_path(std::filesystem::temp_directory_path(), error); std::filesystem::remove_all(root, error); return 0; diff --git a/core/libs/tests/GuestFopenLongHostPath.cpp b/core/libs/tests/GuestFopenLongHostPath.cpp index 9a15819f63..8b79945a60 100644 --- a/core/libs/tests/GuestFopenLongHostPath.cpp +++ b/core/libs/tests/GuestFopenLongHostPath.cpp @@ -8,7 +8,9 @@ #include #include #include +#ifndef NOMINMAX #define NOMINMAX +#endif #include namespace { diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index c14954f91c..ed1987ef28 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -1,6 +1,6 @@ #include "prx/libSceFreeType/include/Api.hpp" #include "FreeTypeFontFixture.hpp" -#include "prx/libSceFreeType/include/GuestFreeType.hpp" +#include "prx/libSceFreeType/include/GuestObjects.hpp" #include "prx/libc/include/General.hpp" #include @@ -9,6 +9,8 @@ #include FT_OUTLINE_H #include FT_TRIGONOMETRY_H #include FT_CID_H +#include FT_SIZES_H +#include FT_SYSTEM_H #include #include @@ -466,8 +468,246 @@ static void Trigonometry() { scalarCases, vectorCases, differenceCases, rendered); } +struct DecomposeEvents { + std::vector> calls; + std::size_t stopAt = static_cast(-1); + int result = 0; + + int Add(int kind, GuestVector first, GuestVector second = {}, GuestVector third = {}) { + calls.push_back({kind, first.x, first.y, second.x, second.y, third.x, third.y}); + return calls.size() - 1 == stopAt ? result : 0; + } +}; + +static GuestVector GuestPoint(const FT_Vector& point) { return {point.x, point.y}; } + +static int NativeMove(const FT_Vector* to, void* user) { return static_cast(user)->Add(1, GuestPoint(*to)); } +static int NativeLine(const FT_Vector* to, void* user) { return static_cast(user)->Add(2, GuestPoint(*to)); } +static int NativeConic(const FT_Vector* control, const FT_Vector* to, void* user) { + return static_cast(user)->Add(3, GuestPoint(*control), GuestPoint(*to)); +} +static int NativeCubic(const FT_Vector* first, const FT_Vector* second, const FT_Vector* to, void* user) { + return static_cast(user)->Add(4, GuestPoint(*first), GuestPoint(*second), GuestPoint(*to)); +} + +static int APS5_VABI GuestMove(const GuestVector* to, void* user) { return static_cast(user)->Add(1, *to); } +static int APS5_VABI GuestLine(const GuestVector* to, void* user) { return static_cast(user)->Add(2, *to); } +static int APS5_VABI GuestConic(const GuestVector* control, const GuestVector* to, void* user) { + return static_cast(user)->Add(3, *control, *to); +} +static int APS5_VABI GuestCubic(const GuestVector* first, const GuestVector* second, const GuestVector* to, void* user) { + return static_cast(user)->Add(4, *first, *second, *to); +} + +static int APS5_VABI NullUserMove(const GuestVector*, void* user) { + Require(user == nullptr); + return -17; +} + +static std::size_t CompareDecomposition(std::vector points, std::vector tags, + std::vector contours, int shift, std::int64_t delta, + std::size_t stopAt = static_cast(-1), int stopResult = 0) { + std::vector nativePoints; + for (const auto point : points) nativePoints.push_back({static_cast(point.x), static_cast(point.y)}); + GuestOutline outline{static_cast(contours.size()), static_cast(points.size()), points.data(), tags.data(), contours.data(), 0}; + FT_Outline native{outline.n_contours, outline.n_points, nativePoints.data(), tags.data(), contours.data(), 0}; + DecomposeEvents actual; + DecomposeEvents expected; + actual.stopAt = expected.stopAt = stopAt; + actual.result = expected.result = stopResult; + const GuestOutlineFuncs functions{GuestMove, GuestLine, GuestConic, GuestCubic, shift, delta}; + const FT_Outline_Funcs nativeFunctions{NativeMove, NativeLine, NativeConic, NativeCubic, shift, static_cast(delta)}; + const int result = FT_Outline_Decompose_nid_postfix(&outline, &functions, &actual); + Require(result == FT_Outline_Decompose(&native, &nativeFunctions, &expected)); + Require(actual.calls == expected.calls); + for (std::size_t index = 0; index < points.size(); ++index) { + Require(points[index].x == nativePoints[index].x && points[index].y == nativePoints[index].y); + } + if (stopAt < expected.calls.size()) Require(result == stopResult && actual.calls.size() == stopAt + 1); + return actual.calls.size(); +} + +struct ChangingDecomposition { + DecomposeEvents events; + GuestOutlineFuncs guest{}; + FT_Outline_Funcs native{}; + GuestOutline* outline = nullptr; + FT_Outline* nativeOutline = nullptr; + GuestVector* replacement = nullptr; + FT_Vector* nativeReplacement = nullptr; +}; + +static int NativeChangedLine(const FT_Vector* to, void* user) { + return static_cast(user)->events.Add(5, GuestPoint(*to)); +} +static int APS5_VABI GuestChangedLine(const GuestVector* to, void* user) { + return static_cast(user)->events.Add(5, *to); +} +static int NativeChangingMove(const FT_Vector* to, void* user) { + auto& state = *static_cast(user); + state.native.line_to = NativeChangedLine; + state.native.shift = 4; + state.native.delta = 999; + state.nativeOutline->points = state.nativeReplacement; + return state.events.Add(1, GuestPoint(*to)); +} +static int APS5_VABI GuestChangingMove(const GuestVector* to, void* user) { + auto& state = *static_cast(user); + state.guest.line_to = GuestChangedLine; + state.guest.shift = 4; + state.guest.delta = 999; + state.outline->points = state.replacement; + GuestOutline empty{}; + const GuestOutlineFuncs noCallbacks{}; + Require(FT_Outline_Decompose_nid_postfix(&empty, &noCallbacks, nullptr) == 0); + return state.events.Add(1, *to); +} + +static void OutlineDecomposition() { + GuestOutline empty{}; + const GuestOutlineFuncs callbacks{GuestMove, GuestLine, GuestConic, GuestCubic, 0, 0}; + const FT_Outline_Funcs nativeCallbacks{NativeMove, NativeLine, NativeConic, NativeCubic, 0, 0}; + Require(FT_Outline_Decompose_nid_postfix(nullptr, nullptr, nullptr) == FT_Outline_Decompose(nullptr, nullptr, nullptr)); + Require(FT_Outline_Decompose_nid_postfix(nullptr, &callbacks, nullptr) == FT_Outline_Decompose(nullptr, &nativeCallbacks, nullptr)); + Require(FT_Outline_Decompose_nid_postfix(&empty, nullptr, nullptr) == FT_Err_Invalid_Argument); + const GuestOutlineFuncs unused{nullptr, nullptr, nullptr, nullptr, -1, INT64_MIN}; + Require(FT_Outline_Decompose_nid_postfix(&empty, &unused, nullptr) == 0); + int scenarios = 0; + std::size_t calls = 0; + for (unsigned count = 1; count <= 6; ++count) { + std::vector points; + for (unsigned point = 0; point < count; ++point) points.push_back({static_cast(point) * 13 - 37, static_cast(point) * -17 + 23}); + const unsigned patterns = 1u << (2 * count); + for (unsigned pattern = 0; pattern < patterns; ++pattern) { + std::vector tags; + for (unsigned point = 0; point < count; ++point) tags.push_back(static_cast(((pattern >> (point * 2)) & 3) | 0xA4)); + for (const int shift : {0, 1, 4}) { + for (const int delta : {-17, 0, 19}) { + calls += CompareDecomposition(points, tags, {static_cast(count - 1)}, shift, delta); + ++scenarios; + } + } + } + } + const std::vector curve{{-17, 3}, {7, -5}, {29, 31}, {43, -11}, {59, 37}, {73, -13}, {89, 41}, {101, -17}}; + for (const auto& tags : std::vector>{{1, 0, 0, 1, 2, 2, 1, 0}, {1, 2, 2, 1, 0, 1, 2, 2}, {0, 0, 0, 0, 0, 0, 0, 0}}) { + for (const auto& contours : std::vector>{{7}, {2, 7}, {0, 3, 7}, {2, 1}}) { + const std::size_t events = CompareDecomposition(curve, tags, contours, 2, -3); + ++scenarios; + for (std::size_t stop = 0; stop < events; ++stop) { + for (const int error : {1, -17, 0x12345678, INT32_MIN}) { + calls += CompareDecomposition(curve, tags, contours, 2, -3, stop, error); + ++scenarios; + } + } + } + } + { + GuestVector points[]{{7, -5}, {11, 13}, {17, -19}, {23, 29}}; + FT_Vector nativePoints[]{{7, -5}, {11, 13}, {17, -19}, {23, 29}}; + GuestVector replacement[]{{107, 95}, {111, 113}, {117, 81}, {123, 129}}; + FT_Vector nativeReplacement[]{{107, 95}, {111, 113}, {117, 81}, {123, 129}}; + unsigned char tags[]{1, 1, 1, 1}; + unsigned short contours[]{1, 3}; + GuestOutline outline{2, 4, points, tags, contours, 0}; + FT_Outline native{2, 4, nativePoints, tags, contours, 0}; + ChangingDecomposition actual; + ChangingDecomposition expected; + actual.guest = {GuestChangingMove, nullptr, nullptr, nullptr, 1, -3}; + expected.native = {NativeChangingMove, nullptr, nullptr, nullptr, 1, -3}; + actual.outline = &outline; + actual.replacement = replacement; + expected.nativeOutline = &native; + expected.nativeReplacement = nativeReplacement; + Require(FT_Outline_Decompose_nid_postfix(&outline, &actual.guest, &actual) == FT_Outline_Decompose(&native, &expected.native, &expected)); + Require(actual.events.calls == expected.events.calls && actual.events.calls.size() == 6); + } + { + GuestVector point{7, -5}; + unsigned char tag = 1; + unsigned short contour = 0; + GuestOutline outline{1, 1, &point, &tag, &contour, 0}; + const GuestOutlineFuncs nullUser{NullUserMove, nullptr, nullptr, nullptr, 0, 0}; + Require(FT_Outline_Decompose_nid_postfix(&outline, &nullUser, nullptr) == -17); + } + { + GuestVector points[]{{0x10000000001ll, -0x10000000003ll}, {0x10000000005ll, -0x10000000007ll}, {0x10000000009ll, -0x1000000000Bll}}; + unsigned char tags[]{1, 1, 1}; + unsigned short contours[]{1}; + GuestOutline outline{1, 2, points, tags, contours, 0}; + auto functions = callbacks; + functions.shift = 2; + functions.delta = 0x100000000ll; + DecomposeEvents events; + Require(FT_Outline_Decompose_nid_postfix(&outline, &functions, &events) == 0); + const std::vector> expected{{1, 0x3FF00000004ll, -0x4010000000Cll, 0, 0, 0, 0}, + {2, 0x3FF00000014ll, -0x4010000001Cll, 0, 0, 0, 0}, + {2, 0x3FF00000004ll, -0x4010000000Cll, 0, 0, 0, 0}}; + Require(events.calls == expected); + tags[0] = tags[1] = 0; + events.calls.clear(); + Require(FT_Outline_Decompose_nid_postfix(&outline, &functions, &events) == 0); + const std::vector> conic{{1, 0x3FF0000000Cll, -0x40100000014ll, 0, 0, 0, 0}, + {3, 0x3FF00000004ll, -0x4010000000Cll, 0x3FF0000000Cll, -0x40100000014ll, 0, 0}, + {3, 0x3FF00000014ll, -0x4010000001Cll, 0x3FF0000000Cll, -0x40100000014ll, 0, 0}}; + Require(events.calls == conic); + tags[0] = 1; + tags[1] = tags[2] = 2; + contours[0] = 2; + outline.n_points = 3; + events.calls.clear(); + Require(FT_Outline_Decompose_nid_postfix(&outline, &functions, &events) == 0); + const std::vector> cubic{{1, 0x3FF00000004ll, -0x4010000000Cll, 0, 0, 0, 0}, + {4, 0x3FF00000014ll, -0x4010000001Cll, 0x3FF00000024ll, -0x4010000002Cll, 0x3FF00000004ll, -0x4010000000Cll}}; + Require(events.calls == cubic); + contours[0] = 1; + outline.n_points = 2; + for (const int shift : {-1, 63, INT32_MAX}) { + functions.shift = shift; + Require(ThrowsWith([&] { FT_Outline_Decompose_nid_postfix(&outline, &functions, &events); }, "FT_Outline_Decompose: a shift")); + } + functions.shift = 62; + functions.delta = 0; + Require(ThrowsWith([&] { FT_Outline_Decompose_nid_postfix(&outline, &functions, &events); }, "FT_Outline_Decompose: overflowing coordinate scaling")); + functions.shift = 0; + functions.delta = INT64_MIN; + Require(ThrowsWith([&] { FT_Outline_Decompose_nid_postfix(&outline, &functions, &events); }, "FT_Outline_Decompose: overflowing coordinate translation")); + functions.delta = 0; + points[0] = {INT64_MAX, 0}; + points[1] = {INT64_MAX, 0}; + tags[0] = tags[1] = 0; + Require(ThrowsWith([&] { FT_Outline_Decompose_nid_postfix(&outline, &functions, &events); }, "FT_Outline_Decompose: overflowing midpoint")); + } + const auto font = SquareGlyphFont('A', 700); + GuestLibrary* library = nullptr; + FT_Library oracle = nullptr; + GuestFaceRec* face = nullptr; + FT_Face native = nullptr; + Require(FT_Init_FreeType_nid_postfix(&library) == 0 && FT_Init_FreeType(&oracle) == 0); + Require(FT_New_Memory_Face_nid_postfix(library, font.data(), font.size(), 0, &face) == 0); + Require(FT_New_Memory_Face(oracle, font.data(), static_cast(font.size()), 0, &native) == 0); + int rendered = 0; + for (const unsigned pixels : {8u, 16u, 31u, 100u}) { + Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, pixels) == FT_Set_Pixel_Sizes(native, 0, pixels)); + for (const unsigned glyph : {0u, 1u}) { + Require(FT_Load_Glyph_nid_postfix(face, glyph, FT_LOAD_DEFAULT) == FT_Load_Glyph(native, glyph, FT_LOAD_DEFAULT)); + DecomposeEvents actual; + DecomposeEvents expected; + Require(FT_Outline_Decompose_nid_postfix(&face->glyph->outline, &callbacks, &actual) == FT_Outline_Decompose(&native->glyph->outline, &nativeCallbacks, &expected)); + Require(actual.calls == expected.calls); + SlotMatches(face->glyph, native->glyph); + Require(FT_Render_Glyph_nid_postfix(face->glyph, FT_RENDER_MODE_NORMAL) == FT_Render_Glyph(native->glyph, FT_RENDER_MODE_NORMAL)); + SlotMatches(face->glyph, native->glyph); + ++rendered; + } + } + Require(FT_Done_FreeType_nid_postfix(library) == 0 && FT_Done_FreeType(oracle) == 0); + std::printf("FreeType outline decomposition: %d native comparisons, %zu callbacks, %d rendered glyphs, wide coordinates and callback mutation\n", scenarios, calls, rendered); +} + struct Finalizers { std::vector calls; + bool reenter = false; }; static std::vector CidMetadataFont(bool keyed, const std::string& registry, const std::string& ordering, @@ -639,7 +879,13 @@ static void NativeSizeFinalizer(void* object) { static void NativeFaceFinalizer(void* object) { auto* face = static_cast(object); Require(face->glyph == nullptr && face->size == nullptr); - static_cast(face->generic.data)->calls.push_back(3); + auto* events = static_cast(face->generic.data); + events->calls.push_back(3); + if (events->reenter) { + Require(FT_Reference_Face(face) == 0 && FT_Reference_Face(face) == 0); + Require(FT_Done_Face(face) == 0); + Require(face->glyph == nullptr && face->size == nullptr); + } } static void APS5_VABI GuestSlotFinalizer(void* object) { @@ -657,7 +903,109 @@ static void APS5_VABI GuestSizeFinalizer(void* object) { static void APS5_VABI GuestFaceFinalizer(void* object) { auto* face = static_cast(object); Require(face->glyph == nullptr && face->size == nullptr); - static_cast(face->generic.data)->calls.push_back(3); + auto* events = static_cast(face->generic.data); + events->calls.push_back(3); + if (events->reenter) { + const auto* library = static_cast(face->internal)->library; + Require(FT_Reference_Face_nid_postfix(face) == 0 && FT_Reference_Face_nid_postfix(face) == 0); + Require(FT_Done_Face_nid_postfix(face) == 0 && library->faces.size() == 1); + Require(face->glyph == nullptr && face->size == nullptr); + } +} + +static std::string BitmapStrikeFont(int width, int height, int descent) { + std::string font = "STARTFONT 2.1\nFONT -anyps5-strike-medium-r-normal--" + std::to_string(height) + "-0-72-72-c-0-iso10646-1\n"; + font += "SIZE " + std::to_string(height) + " 72 72\nFONTBOUNDINGBOX " + std::to_string(width) + " " + std::to_string(height) + " 0 -" + std::to_string(descent); + font += "\nSTARTPROPERTIES 4\nFONT_ASCENT " + std::to_string(height - descent) + "\nFONT_DESCENT " + std::to_string(descent); + font += "\nCHARSET_REGISTRY \"ISO10646\"\nCHARSET_ENCODING \"1\"\nENDPROPERTIES\nCHARS 1\nSTARTCHAR A\nENCODING 65\nSWIDTH 1000 0\nDWIDTH "; + font += std::to_string(width) + " 0\nBBX " + std::to_string(width) + " " + std::to_string(height) + " 0 -" + std::to_string(descent) + "\nBITMAP\n"; + for (int y = 0; y < height; ++y) { + for (int byte = 0; byte < (width + 7) / 8; ++byte) { + unsigned value = 0; + for (int bit = 0; bit < 8; ++bit) { + const int x = byte * 8 + bit; + if (x < width && (x == 0 || y == height - 1 || (x + y) % 3 == 0)) value |= 0x80u >> bit; + } + font += "0123456789ABCDEF"[value >> 4]; + font += "0123456789ABCDEF"[value & 15]; + } + font += '\n'; + } + return font + "ENDCHAR\nENDFONT\n"; +} + +static void CheckStrikeSelection(GuestLibrary* library, FT_Library nativeLibrary, GuestFaceRec* scalable, FT_Face nativeScalable) { + const auto sizeMatches = [](const GuestSizeRec* size, FT_Size host) { + const auto& actual = size->metrics; + const auto& expected = host->metrics; + Require(actual.x_ppem == expected.x_ppem && actual.y_ppem == expected.y_ppem); + Require(actual.x_scale == expected.x_scale && actual.y_scale == expected.y_scale); + Require(actual.ascender == expected.ascender && actual.descender == expected.descender); + Require(actual.height == expected.height && actual.max_advance == expected.max_advance); + }; + constexpr std::array invalidIndices{-1, 1, 2, std::numeric_limits::min(), std::numeric_limits::max()}; + int requests = 0; + int rendered = 0; + for (const auto index : invalidIndices) { + const auto nullError = FT_Select_Size(nullptr, index); + Require(nullError != 0 && FT_Select_Size_nid_postfix(nullptr, index) == nullError); + ++requests; + } + for (const auto index : {0, -1, std::numeric_limits::max()}) { + const auto error = FT_Select_Size(nativeScalable, index); + Require(error == FT_Err_Invalid_Face_Handle && FT_Select_Size_nid_postfix(scalable, index) == error); + sizeMatches(scalable->size, nativeScalable->size); + ++requests; + } + for (const auto& dimensions : {std::array{5, 7, 2}, std::array{11, 13, 3}}) { + const auto [width, height, descent] = dimensions; + const std::string font = BitmapStrikeFont(width, height, descent); + const auto* bytes = reinterpret_cast(font.data()); + GuestFaceRec* face = nullptr; + FT_Face native = nullptr; + Require(FT_New_Memory_Face_nid_postfix(library, bytes, static_cast(font.size()), 0, &face) == 0); + Require(FT_New_Memory_Face(nativeLibrary, bytes, static_cast(font.size()), 0, &native) == 0); + Require(FT_HAS_FIXED_SIZES(native) && !FT_IS_SCALABLE(native) && face->num_fixed_sizes == 1); + const GuestBitmapSize* strikes = face->available_sizes; + const auto& expectedStrike = native->available_sizes[0]; + Require(strikes[0].width == expectedStrike.width && strikes[0].height == expectedStrike.height && strikes[0].size == expectedStrike.size); + Require(strikes[0].x_ppem == expectedStrike.x_ppem && strikes[0].y_ppem == expectedStrike.y_ppem); + GuestSizeRec* size = face->size; + int marker = 0; + size->generic.data = ▮ + sizeMatches(size, native->size); + Require(size->metrics.y_ppem == 0); + const std::array indices{-1, 1, invalidIndices[3], invalidIndices[4], 0, 2, 0, -1, 0}; + for (const auto index : indices) { + face->glyph->advance.x = 0x123456789ABCll; + face->bbox.xMin = -0x123456789ABCll; + const auto error = FT_Select_Size(native, index); + Require((index == 0) == (error == 0)); + Require(FT_Select_Size_nid_postfix(face, index) == error); + ++requests; + sizeMatches(size, native->size); + Require(face->size == size && size->face == face && size->generic.data == &marker && face->available_sizes == strikes); + Require(face->glyph->advance.x == 0x123456789ABCll && face->bbox.xMin == -0x123456789ABCll); + if (error != 0) continue; + Require(size->metrics.y_ppem == height && size->metrics.ascender == (height - descent) * 64 && size->metrics.descender == -descent * 64); + for (const auto flags : std::array{FT_LOAD_DEFAULT, FT_LOAD_RENDER, FT_LOAD_RENDER | FT_LOAD_MONOCHROME}) { + Require(FT_Load_Char_nid_postfix(face, 'A', flags) == 0 && FT_Load_Char(native, 'A', flags) == 0); + SlotMatches(face->glyph, native->glyph); + const auto& bitmap = face->glyph->bitmap; + Require(face->glyph->format == FT_GLYPH_FORMAT_BITMAP && bitmap.pixel_mode == FT_PIXEL_MODE_MONO); + Require(bitmap.width == width && bitmap.rows == height && bitmap.pitch > 0 && face->glyph->bitmap_top == height - descent); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + const bool pixel = (bitmap.buffer[y * bitmap.pitch + x / 8] & (0x80u >> (x % 8))) != 0; + Require(pixel == (x == 0 || y == height - 1 || (x + y) % 3 == 0)); + } + } + ++rendered; + } + } + Require(FT_Done_Face_nid_postfix(face) == 0 && FT_Done_Face(native) == 0); + } + std::printf("FT_Select_Size: %d native request comparisons, %d bitmap glyph comparisons\n", requests, rendered); } static void SetFinalizers(FT_Face face, Finalizers& events) { @@ -666,6 +1014,156 @@ static void SetFinalizers(FT_Face face, Finalizers& events) { face->size->generic = {&events, NativeSizeFinalizer}; } +static bool Equal(GuestVector a, GuestVector b) { + return a.x == b.x && a.y == b.y; +} + +static bool Equal(const GuestOutline& a, const GuestOutline& b) { + return a.n_points == b.n_points && a.n_contours == b.n_contours && a.points == b.points && + a.tags == b.tags && a.contours == b.contours && a.flags == b.flags; +} + +static void CopyCase(std::uint16_t pointCount, std::uint16_t contourCount, int sourceFlags, int targetFlags, bool wide) { + const GuestVector guard{0x123456789ABCDEF, -0x123456789ABCDEF}; + std::vector points(pointCount + 2, guard), copied(pointCount + 2, guard); + std::vector tags(pointCount + 2, 0xA5), copiedTags(pointCount + 2, 0x5A); + std::vector contours(contourCount + 2, 0xABCD), copiedContours(contourCount + 2, 0xDCBA); + std::vector hostPoints(pointCount), hostCopied(pointCount); + std::vector hostTags(pointCount), hostCopiedTags(pointCount); + std::vector hostContours(contourCount), hostCopiedContours(contourCount); + for (unsigned index = 0; index < pointCount; ++index) { + const auto value = static_cast(index) * 65537 - 100000; + points[index + 1] = wide ? GuestVector{INT64_MIN + index, + INT64_MAX - index} + : GuestVector{value / 4, -value / 4}; + tags[index + 1] = static_cast(index); + hostPoints[index] = {static_cast(points[index + 1].x), static_cast(points[index + 1].y)}; + hostTags[index] = tags[index + 1]; + } + for (unsigned index = 0; index < contourCount; ++index) { + contours[index + 1] = static_cast(index); + hostContours[index] = contours[index + 1]; + } + GuestOutline source{contourCount, pointCount, pointCount ? points.data() + 1 : nullptr, + pointCount ? tags.data() + 1 : nullptr, contourCount ? contours.data() + 1 : nullptr, sourceFlags}; + GuestOutline target{contourCount, pointCount, pointCount ? copied.data() + 1 : nullptr, + pointCount ? copiedTags.data() + 1 : nullptr, contourCount ? copiedContours.data() + 1 : nullptr, targetFlags}; + const auto originalSource = source; + auto expectedTarget = target; + const auto originalPoints = points; + const auto originalTags = tags; + const auto originalContours = contours; + FT_Outline hostSource{contourCount, pointCount, hostPoints.data(), hostTags.data(), hostContours.data(), sourceFlags}; + FT_Outline hostTarget{contourCount, pointCount, hostCopied.data(), hostCopiedTags.data(), hostCopiedContours.data(), targetFlags}; + Require(FT_Outline_Copy_nid_postfix(&source, &target) == FT_Outline_Copy(&hostSource, &hostTarget)); + expectedTarget.flags = hostTarget.flags; + Require(Equal(source, originalSource) && Equal(target, expectedTarget)); + Require(Equal(copied.front(), guard) && Equal(copied.back(), guard)); + Require(copiedTags.front() == 0x5A && copiedTags.back() == 0x5A); + Require(copiedContours.front() == 0xDCBA && copiedContours.back() == 0xDCBA); + for (unsigned index = 0; index < pointCount; ++index) { + Require(Equal(copied[index + 1], originalPoints[index + 1])); + if (!wide) Require(copied[index + 1].x == hostCopied[index].x && copied[index + 1].y == hostCopied[index].y); + Require(copiedTags[index + 1] == hostCopiedTags[index]); + } + for (unsigned index = 0; index < contourCount; ++index) Require(copiedContours[index + 1] == hostCopiedContours[index]); + if (pointCount) { + target.points[0] = {321, -654}; + target.tags[0] ^= 0xFF; + } + if (contourCount) target.contours[0] ^= 0xFFFF; + for (std::size_t index = 0; index < points.size(); ++index) Require(Equal(points[index], originalPoints[index])); + Require(tags == originalTags && contours == originalContours); +} + +static void InvalidAndSelfCopies() { + GuestOutline source{1, 4, nullptr, nullptr, nullptr, 0x1234}; + auto target = source; + Require(FT_Outline_Copy_nid_postfix(nullptr, nullptr) == FT_Outline_Copy(nullptr, nullptr)); + Require(FT_Outline_Copy_nid_postfix(nullptr, &target) == FT_Err_Invalid_Outline); + Require(Equal(target, source)); + Require(FT_Outline_Copy_nid_postfix(&source, nullptr) == FT_Err_Invalid_Outline); + Require(FT_Outline_Copy_nid_postfix(&source, &source) == 0); + for (const auto counts : std::array, 3>{{{1, 3}, {0, 4}, {0, 0}}}) { + target.n_contours = counts[0]; + target.n_points = counts[1]; + const auto before = target; + FT_Outline nativeSource{1, 4, nullptr, nullptr, nullptr, source.flags}; + FT_Outline nativeTarget{target.n_contours, target.n_points, nullptr, nullptr, nullptr, target.flags}; + Require(FT_Outline_Copy_nid_postfix(&source, &target) == FT_Outline_Copy(&nativeSource, &nativeTarget)); + Require(Equal(target, before)); + } +} + +static void RenderCopy() { + const auto font = SquareGlyphFont('A', 700); + GuestLibrary* library = nullptr; + GuestFaceRec* face = nullptr; + FT_Library nativeLibrary = nullptr; + FT_Face nativeFace = nullptr; + Require(FT_Init_FreeType_nid_postfix(&library) == 0 && FT_Init_FreeType(&nativeLibrary) == 0); + Require(FT_New_Memory_Face_nid_postfix(library, font.data(), font.size(), 0, &face) == 0); + Require(FT_New_Memory_Face(nativeLibrary, font.data(), static_cast(font.size()), 0, &nativeFace) == 0); + Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, 32) == 0 && FT_Set_Pixel_Sizes(nativeFace, 0, 32) == 0); + for (const int offset : {0, 37, -129}) { + Require(FT_Load_Char_nid_postfix(face, 'A', FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP) == 0); + Require(FT_Load_Char(nativeFace, 'A', FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP) == 0); + auto& target = face->glyph->outline; + std::vector points(target.points, target.points + target.n_points); + std::vector nativePoints(nativeFace->glyph->outline.points, + nativeFace->glyph->outline.points + nativeFace->glyph->outline.n_points); + for (std::size_t index = 0; index < points.size(); ++index) { + points[index].x += offset + points[index].y / 4; + nativePoints[index].x += offset + nativePoints[index].y / 4; + } + auto source = target; + source.points = points.data(); + source.flags ^= FT_OUTLINE_OWNER; + auto nativeSource = nativeFace->glyph->outline; + nativeSource.points = nativePoints.data(); + nativeSource.flags ^= FT_OUTLINE_OWNER; + Require(FT_Outline_Copy_nid_postfix(&source, &target) == 0); + Require(FT_Outline_Copy(&nativeSource, &nativeFace->glyph->outline) == 0); + points[0] = {}; + nativePoints[0] = {}; + Require(FT_Render_Glyph_nid_postfix(face->glyph, FT_RENDER_MODE_NORMAL) == 0); + Require(FT_Render_Glyph(nativeFace->glyph, FT_RENDER_MODE_NORMAL) == 0); + const auto& actual = face->glyph->bitmap; + const auto& expected = nativeFace->glyph->bitmap; + Require(actual.rows > 0 && actual.width > 0); + Require(actual.rows == expected.rows && actual.width == expected.width && actual.pitch == expected.pitch); + Require(actual.pixel_mode == expected.pixel_mode && actual.num_grays == expected.num_grays); + Require(face->glyph->bitmap_left == nativeFace->glyph->bitmap_left && face->glyph->bitmap_top == nativeFace->glyph->bitmap_top); + const auto size = static_cast(actual.rows) * std::abs(actual.pitch); + Require(std::memcmp(actual.buffer, expected.buffer, size) == 0); + } + Require(FT_Done_Face_nid_postfix(face) == 0 && FT_Done_Face(nativeFace) == 0); + Require(FT_Done_FreeType_nid_postfix(library) == 0 && FT_Done_FreeType(nativeLibrary) == 0); +} + +static void CheckOutlineCopies() { + InvalidAndSelfCopies(); + for (const int sourceFlags : {0, FT_OUTLINE_OWNER, FT_OUTLINE_REVERSE_FILL, -1}) { + for (const int targetFlags : {0, FT_OUTLINE_OWNER, FT_OUTLINE_EVEN_ODD_FILL, -1}) { + CopyCase(0, 0, sourceFlags, targetFlags, false); + CopyCase(4, 2, sourceFlags, targetFlags, false); + CopyCase(4, 2, sourceFlags, targetFlags, true); + } + } + CopyCase(65535, 65535, FT_OUTLINE_REVERSE_FILL, FT_OUTLINE_OWNER, false); + CopyCase(65535, 65535, FT_OUTLINE_OWNER, 0, true); + std::mt19937 random(0xF7C0); + for (unsigned index = 0; index < 128; ++index) { + const auto count = static_cast(random() % 256 + 1); + const auto contours = static_cast(random() % (count + 1)); + const auto sourceFlags = static_cast(random() & 0x7FFFFFFFu); + const auto targetFlags = static_cast(random() & 0x7FFFFFFFu); + CopyCase(count, contours, sourceFlags, targetFlags, false); + } + RenderCopy(); + std::puts("FreeType outline copy: 178 copy cases, validation, self-copy and 3 rendered copies passed"); +} + static void SetFinalizers(GuestFaceRec* face, Finalizers& events) { face->generic = {&events, GuestFaceFinalizer}; face->glyph->generic = {&events, GuestSlotFinalizer}; @@ -761,12 +1259,332 @@ static void CheckTransforms(const std::vector& font) { std::printf("FT_Set_Transform: %zu native glyph and bitmap comparisons\n", comparisons); } +static void SizeMatches(const GuestSizeRec* size, FT_Size native) { + Require(size != nullptr && native != nullptr); + const auto& a = size->metrics; + const auto& b = native->metrics; + Require(a.x_ppem == b.x_ppem && a.y_ppem == b.y_ppem && a.x_scale == b.x_scale && a.y_scale == b.y_scale); + Require(a.ascender == b.ascender && a.descender == b.descender && a.height == b.height && a.max_advance == b.max_advance); +} + +static bool OwnsSize(const GuestFace& face, const GuestSizeRec* size) { + if (size == &face.size) return true; + for (const auto& entry : face.sizes) { + if (size == &entry.rec) return true; + } + return false; +} + +struct SizeEvents { + std::vector sizes; + std::vector> calls; + + int Index(void* size) const { + if (!size) return -1; + for (std::size_t index = 0; index < sizes.size(); ++index) { + if (sizes[index] == size) return static_cast(index); + } + Require(false); + return -2; + } +}; + +static void NativeMultipleSizeFinalizer(void* object) { + auto* size = static_cast(object); + auto& events = *static_cast(size->generic.data); + events.calls.push_back({events.Index(size), events.Index(size->face->size), size->metrics.y_ppem, size->face->glyph == nullptr}); +} + +static void APS5_VABI GuestMultipleSizeFinalizer(void* object) { + auto* size = static_cast(object); + auto* face = static_cast(size->face); + const auto& owner = *static_cast(face->internal); + Require(OwnsSize(owner, size)); + if (face->size) Require(OwnsSize(owner, face->size)); + SizeMatches(size, static_cast(size->internal)); + auto& events = *static_cast(size->generic.data); + events.calls.push_back({events.Index(size), events.Index(face->size), size->metrics.y_ppem, face->glyph == nullptr}); +} + +static void NativeMultipleSizeFaceFinalizer(void* object) { + auto* face = static_cast(object); + Require(face->glyph == nullptr && face->size == nullptr); + static_cast(face->generic.data)->calls.push_back({100, -1, 0, 1}); +} + +static void APS5_VABI GuestMultipleSizeFaceFinalizer(void* object) { + auto* face = static_cast(object); + Require(face->glyph == nullptr && face->size == nullptr); + static_cast(face->generic.data)->calls.push_back({100, -1, 0, 1}); +} + +static FT_Alloc_Func SizeOriginalAlloc = nullptr; +static int SizeAllocationsBeforeFailure = 0; + +static void* SizeFailingAlloc(FT_Memory memory, long bytes) { + if (SizeAllocationsBeforeFailure-- == 0) return nullptr; + return SizeOriginalAlloc(memory, bytes); +} + +static void SizeObjects(const std::vector& font) { + Require(FT_New_Size_nid_postfix(nullptr, nullptr) == FT_New_Size(nullptr, nullptr)); + Require(FT_Done_Size_nid_postfix(nullptr) == FT_Done_Size(nullptr)); + Require(FT_Activate_Size_nid_postfix(nullptr) == FT_Activate_Size(nullptr)); + GuestSizeRec sentinel{}; + GuestSizeRec* untouched = &sentinel; + Require(FT_New_Size_nid_postfix(nullptr, &untouched) == FT_Err_Invalid_Face_Handle && untouched == &sentinel); + FT_FaceRec driverless{}; + GuestFace invalidOwner; + invalidOwner.host = &driverless; + invalidOwner.rec.internal = &invalidOwner; + FT_Size nativeUntouched = reinterpret_cast(&sentinel); + Require(FT_New_Size(&driverless, &nativeUntouched) == FT_Err_Invalid_Driver_Handle); + Require(FT_New_Size_nid_postfix(&invalidOwner.rec, &untouched) == FT_Err_Invalid_Driver_Handle && untouched == &sentinel); + Require(FT_New_Size_nid_postfix(&invalidOwner.rec, nullptr) == FT_New_Size(&driverless, nullptr)); + + FT_SizeRec driverlessSize{}; + GuestSizeRec invalidSize{}; + invalidSize.internal = &driverlessSize; + Require(FT_Done_Size_nid_postfix(&invalidSize) == FT_Done_Size(&driverlessSize)); + Require(FT_Activate_Size_nid_postfix(&invalidSize) == FT_Activate_Size(&driverlessSize)); + driverlessSize.face = &driverless; + Require(FT_Done_Size_nid_postfix(&invalidSize) == FT_Done_Size(&driverlessSize)); + Require(FT_Activate_Size_nid_postfix(&invalidSize) == FT_Activate_Size(&driverlessSize)); + static_assert(sizeof(GuestSizeMetrics) == 56 && sizeof(GuestSizeRec) == 88); + + int scenarios = 0; + int rendered = 0; + int failures = 0; + for (const int faceIndex : {0, -1}) { + for (const int count : {0, 1, 5, 41}) { + for (const int cleanup : {0, 1, 2}) { + GuestLibrary* library = nullptr; + FT_Library oracle = nullptr; + Require(FT_Init_FreeType_nid_postfix(&library) == 0 && FT_Init_FreeType(&oracle) == 0); + GuestFaceRec* face = nullptr; + FT_Face native = nullptr; + Require(FT_New_Memory_Face_nid_postfix(library, font.data(), font.size(), faceIndex, &face) == 0); + Require(FT_New_Memory_Face(oracle, font.data(), static_cast(font.size()), faceIndex, &native) == 0); + SizeEvents actual; + SizeEvents expected; + face->generic = {&actual, GuestMultipleSizeFaceFinalizer}; + native->generic = {&expected, NativeMultipleSizeFaceFinalizer}; + std::vector sizes; + std::vector originals; + auto remember = [&](GuestSizeRec* size, FT_Size original) { + Require(size->face == face && original->face == native); + actual.sizes.push_back(size); + expected.sizes.push_back(original); + size->generic = {&actual, GuestMultipleSizeFinalizer}; + original->generic = {&expected, NativeMultipleSizeFinalizer}; + sizes.push_back(size); + originals.push_back(original); + SizeMatches(size, original); + }; + if (native->size) remember(face->size, native->size); + Require(FT_New_Size_nid_postfix(face, nullptr) == FT_New_Size(native, nullptr)); + for (int index = 0; index < count; ++index) { + GuestSizeRec* before = face->size; + FT_Size previous = native->size; + GuestSizeRec* created = nullptr; + FT_Size original = nullptr; + Require(FT_New_Size_nid_postfix(face, &created) == FT_New_Size(native, &original)); + Require(created != nullptr && original != nullptr && face->size == before && native->size == previous); + remember(created, original); + } + if (count == 5) { + Require(FT_New_Size_nid_postfix(face, &face->size) == FT_New_Size(native, &native->size)); + remember(face->size, native->size); + } + for (std::size_t index = 0; index < sizes.size(); ++index) { + Require(FT_Activate_Size_nid_postfix(sizes[index]) == 0 && FT_Activate_Size(originals[index]) == 0); + Require(face->size == sizes[index] && native->size == originals[index]); + Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, 12 + index) == FT_Set_Pixel_Sizes(native, 0, 12 + index)); + SizeMatches(sizes[index], originals[index]); + } + for (std::size_t turn = 0; turn < sizes.size() * 3; ++turn) { + const std::size_t index = (turn * 17 + 3) % sizes.size(); + Require(FT_Activate_Size_nid_postfix(sizes[index]) == FT_Activate_Size(originals[index])); + Require(face->size == sizes[index]); + for (std::size_t other = 0; other < sizes.size(); ++other) SizeMatches(sizes[other], originals[other]); + if (!native->glyph) continue; + Require(FT_Load_Char_nid_postfix(face, 'A', FT_LOAD_NO_HINTING) == FT_Load_Char(native, 'A', FT_LOAD_NO_HINTING)); + for (unsigned point = 0; point < face->glyph->outline.n_points; ++point) face->glyph->outline.points[point].x += 64; + FT_Outline_Translate(&native->glyph->outline, 64, 0); + Require(FT_Activate_Size_nid_postfix(sizes[index]) == FT_Activate_Size(originals[index])); + Require(FT_Render_Glyph_nid_postfix(face->glyph, FT_RENDER_MODE_NORMAL) == FT_Render_Glyph(native->glyph, FT_RENDER_MODE_NORMAL)); + SlotMatches(face->glyph, native->glyph); + ++rendered; + } + if (cleanup == 0) { + while (!sizes.empty()) { + const std::size_t index = sizes.size() % 2 == 0 ? 0 : sizes.size() - 1; + const int removed = actual.Index(sizes[index]); + Require(FT_Done_Size_nid_postfix(sizes[index]) == FT_Done_Size(originals[index])); + actual.sizes[removed] = nullptr; + expected.sizes[removed] = nullptr; + sizes.erase(sizes.begin() + index); + originals.erase(originals.begin() + index); + Require(actual.Index(face->size) == expected.Index(native->size) && actual.calls == expected.calls); + } + Require(face->size == nullptr && native->size == nullptr); + Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, 24) == FT_Set_Pixel_Sizes(native, 0, 24)); + Require(FT_New_Size_nid_postfix(face, &face->size) == FT_New_Size(native, &native->size)); + Require(face->size != nullptr && native->size != nullptr); + remember(face->size, native->size); + Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, 24) == FT_Set_Pixel_Sizes(native, 0, 24)); + for (const bool alias : {false, true}) { + for (const int allocation : {0, 1, 2}) { + auto* owner = static_cast(face->internal); + FT_Memory memory = owner->host->memory; + SizeOriginalAlloc = memory->alloc; + SizeAllocationsBeforeFailure = allocation; + memory->alloc = SizeFailingAlloc; + GuestSizeRec* output = &sentinel; + const int error = FT_New_Size_nid_postfix(face, alias ? &face->size : &output); + memory->alloc = SizeOriginalAlloc; + Require(error == FT_Err_Out_Of_Memory && owner->sizes.size() == 1); + FT_Memory nativeMemory = native->memory; + SizeOriginalAlloc = nativeMemory->alloc; + SizeAllocationsBeforeFailure = allocation; + nativeMemory->alloc = SizeFailingAlloc; + FT_Size nativeOutput = reinterpret_cast(&sentinel); + const int nativeError = FT_New_Size(native, alias ? &native->size : &nativeOutput); + nativeMemory->alloc = SizeOriginalAlloc; + Require(error == nativeError); + Require(alias ? face->size == nullptr && native->size == nullptr : output == nullptr && nativeOutput == nullptr); + Require(actual.Index(face->size) == expected.Index(native->size)); + Require(actual.calls == expected.calls); + ++failures; + } + } + } + if (cleanup == 1) { + Require(FT_Reference_Face_nid_postfix(face) == 0 && FT_Reference_Face(native) == 0); + Require(FT_Done_Face_nid_postfix(face) == 0 && FT_Done_Face(native) == 0); + Require(actual.calls.empty() && expected.calls.empty()); + } + if (cleanup == 2 && count == 5) { + Require(sizes.size() > 2); + Require(FT_Activate_Size_nid_postfix(sizes[1]) == FT_Activate_Size(originals[1])); + Require(face->size == sizes[1] && native->size == originals[1]); + } + if (cleanup != 2) Require(FT_Done_Face_nid_postfix(face) == 0 && FT_Done_Face(native) == 0); + Require(FT_Done_FreeType_nid_postfix(library) == 0 && FT_Done_FreeType(oracle) == 0); + Require(actual.calls == expected.calls && !actual.calls.empty() && actual.calls.back()[0] == 100); + if (cleanup == 2 && count == 5) { + Require(actual.calls.size() > 3 && actual.calls[0][1] == 1 && actual.calls[1][0] == 1); + for (std::size_t index = 2; index + 1 < actual.calls.size(); ++index) Require(actual.calls[index][1] == 1); + } + ++scenarios; + } + } + } + std::printf("FreeType size objects: %d lifecycles, %d rendered glyphs, %d allocation failures compared with native\n", scenarios, rendered, failures); +} + int main() { + SizeObjects(SquareGlyphFont('A', 700)); + { + const auto referenceFont = SquareGlyphFont('A', 700); + GuestLibrary* referenceLibrary = nullptr; + FT_Library oracle = nullptr; + Require(FT_Init_FreeType_nid_postfix(&referenceLibrary) == 0 && FT_Init_FreeType(&oracle) == 0); + auto* owners = referenceLibrary; + Require(FT_Reference_Face_nid_postfix(nullptr) == FT_Reference_Face(nullptr)); + int references = 0; + int retained = 0; + int finalized = 0; + for (const unsigned depth : {1u, 2u, 3u, 17u}) { + GuestFaceRec* shared = nullptr; + FT_Face native = nullptr; + Require(FT_New_Memory_Face_nid_postfix(referenceLibrary, referenceFont.data(), static_cast(referenceFont.size()), 0, &shared) == 0); + Require(FT_New_Memory_Face(oracle, referenceFont.data(), static_cast(referenceFont.size()), 0, &native) == 0); + Finalizers guestEvents; + Finalizers nativeEvents; + guestEvents.reenter = nativeEvents.reenter = depth == 3; + SetFinalizers(shared, guestEvents); + SetFinalizers(native, nativeEvents); + for (unsigned index = 1; index < depth; ++index) { + Require(FT_Reference_Face_nid_postfix(shared) == FT_Reference_Face(native)); + ++references; + } + for (unsigned index = 1; index < depth; ++index) { + Require(FT_Done_Face(native) == 0 && FT_Done_Face_nid_postfix(shared) == 0); + Require(owners->faces.size() == 1 && guestEvents.calls.empty() && nativeEvents.calls.empty()); + Require(FT_Get_Char_Index_nid_postfix(shared, 'A') == FT_Get_Char_Index(native, 'A')); + Require(FT_Set_Pixel_Sizes_nid_postfix(shared, 0, 16 + index) == 0 && FT_Set_Pixel_Sizes(native, 0, 16 + index) == 0); + Require(FT_Load_Char_nid_postfix(shared, 'A', FT_LOAD_RENDER) == 0 && FT_Load_Char(native, 'A', FT_LOAD_RENDER) == 0); + SlotMatches(shared->glyph, native->glyph); + ++retained; + } + Require(FT_Done_Face(native) == 0 && FT_Done_Face_nid_postfix(shared) == 0); + if (guestEvents.reenter) { + references += 2; + ++retained; + } + Require(owners->faces.empty() && guestEvents.calls == nativeEvents.calls && guestEvents.calls == std::vector({1, 2, 3})); + ++finalized; + } + GuestFaceRec* plain = nullptr; + FT_Face nativePlain = nullptr; + Require(FT_New_Memory_Face_nid_postfix(referenceLibrary, referenceFont.data(), static_cast(referenceFont.size()), 0, &plain) == 0); + Require(FT_New_Memory_Face(oracle, referenceFont.data(), static_cast(referenceFont.size()), 0, &nativePlain) == 0); + Require(FT_Reference_Face_nid_postfix(plain) == FT_Reference_Face(nativePlain)); + ++references; + Require(FT_Done_Face(nativePlain) == 0 && FT_Done_Face_nid_postfix(plain) == 0 && owners->faces.size() == 1); + ++retained; + Require(FT_Done_Face(nativePlain) == 0 && FT_Done_Face_nid_postfix(plain) == 0 && owners->faces.empty()); + ++finalized; + GuestFaceRec* probe = nullptr; + FT_Face nativeProbe = nullptr; + Require(FT_New_Memory_Face_nid_postfix(referenceLibrary, referenceFont.data(), static_cast(referenceFont.size()), -1, &probe) == 0); + Require(FT_New_Memory_Face(oracle, referenceFont.data(), static_cast(referenceFont.size()), -1, &nativeProbe) == 0); + Finalizers probeEvents; + Finalizers nativeProbeEvents; + probe->generic = {&probeEvents, GuestFaceFinalizer}; + nativeProbe->generic = {&nativeProbeEvents, NativeFaceFinalizer}; + Require(probe->glyph == nullptr && probe->size == nullptr); + Require(FT_Reference_Face_nid_postfix(probe) == FT_Reference_Face(nativeProbe)); + ++references; + Require(FT_Done_Face(nativeProbe) == 0 && FT_Done_Face_nid_postfix(probe) == 0 && owners->faces.size() == 1); + ++retained; + Require(probeEvents.calls.empty() && nativeProbeEvents.calls.empty()); + Require(FT_Done_Face(nativeProbe) == 0 && FT_Done_Face_nid_postfix(probe) == 0); + Require(owners->faces.empty() && probeEvents.calls == nativeProbeEvents.calls && probeEvents.calls == std::vector({3})); + ++finalized; + std::array shutdownEvents; + std::array nativeShutdownEvents; + std::array shutdown{}; + std::array nativeShutdown{}; + for (unsigned index = 0; index < shutdown.size(); ++index) { + Require(FT_New_Memory_Face_nid_postfix(referenceLibrary, referenceFont.data(), static_cast(referenceFont.size()), 0, &shutdown[index]) == 0); + Require(FT_New_Memory_Face(oracle, referenceFont.data(), static_cast(referenceFont.size()), 0, &nativeShutdown[index]) == 0); + SetFinalizers(shutdown[index], shutdownEvents[index]); + SetFinalizers(nativeShutdown[index], nativeShutdownEvents[index]); + for (unsigned reference = 0; reference < 3 - index; ++reference) { + Require(FT_Reference_Face_nid_postfix(shutdown[index]) == FT_Reference_Face(nativeShutdown[index])); + ++references; + } + Require(FT_Done_Face(nativeShutdown[index]) == 0 && FT_Done_Face_nid_postfix(shutdown[index]) == 0); + Require(owners->faces.size() == index + 1 && shutdownEvents[index].calls.empty()); + ++retained; + } + Require(FT_Done_FreeType(oracle) == 0 && FT_Done_FreeType_nid_postfix(referenceLibrary) == 0); + for (unsigned index = 0; index < shutdown.size(); ++index) { + Require(shutdownEvents[index].calls == nativeShutdownEvents[index].calls && shutdownEvents[index].calls == std::vector({1, 2, 3})); + ++finalized; + } + Require(references == 28 && retained == 24 && finalized == 8); + std::printf("FreeType face references: %d retains, %d non-final releases, %d finalized faces compared with native\n", references, retained, finalized); + } const auto root = std::filesystem::temp_directory_path() / ("anyps5_guest_freetype-" + std::to_string(std::random_device{}())); std::filesystem::create_directories(root / "app0"); CheckGasp(); Trigonometry(); + OutlineDecomposition(); const std::vector font = SquareGlyphFont('A', 700); + CheckOutlineCopies(); { std::ofstream file(root / "app0" / "font.ttf", std::ios::binary); file.write(reinterpret_cast(font.data()), static_cast(font.size())); @@ -801,8 +1619,23 @@ int main() { GuestFaceRec* masked = nullptr; Require(FT_New_Face_nid_postfix(library, "/app0/font.ttf", 0x180000000ll, &masked) == 0 && masked != nullptr && masked->face_index == 0); Require(FT_New_Face_nid_postfix(library, "/app0/font.ttf", 0, &face) == 0 && face != nullptr && face != masked); + { + FT_Face nativeFile = nullptr; + Require(FT_New_Face(hostLibrary, "app0/font.ttf", 0, &nativeFile) == 0); + const auto* bytes = FaceOf(face)->bytes.data(); + Require(FaceOf(face)->bytes.size() == font.size() && std::memcmp(bytes, font.data(), font.size()) == 0); + Require(FT_Reference_Face_nid_postfix(face) == FT_Reference_Face(nativeFile)); + Require(FT_Done_Face(nativeFile) == 0 && FT_Done_Face_nid_postfix(face) == 0); + Require(static_cast(library)->faces.size() == 2); + Require(FaceOf(face)->bytes.data() == bytes && std::memcmp(bytes, font.data(), font.size()) == 0); + Require(FT_Get_Char_Index_nid_postfix(face, 'A') == FT_Get_Char_Index(nativeFile, 'A')); + Require(FT_Done_Face(nativeFile) == 0); + std::puts("File-backed face reference: owned font bytes survive a non-final release"); + } FaceMatches(face, host, library); + CheckStrikeSelection(library, hostLibrary, face, host); + const auto fontSize = static_cast(font.size()); GuestFaceRec* memoryFace = nullptr; Require(FT_New_Memory_Face_nid_postfix(library, nullptr, fontSize, 0, &memoryFace) == FT_Err_Invalid_Argument); diff --git a/core/libs/tests/GuestFreeTypeList.cpp b/core/libs/tests/GuestFreeTypeList.cpp index 0e93a0d850..f853e00bbb 100644 --- a/core/libs/tests/GuestFreeTypeList.cpp +++ b/core/libs/tests/GuestFreeTypeList.cpp @@ -7,6 +7,9 @@ #include #include #include +#include +#include +#include FT_LIST_H using namespace FreeType; @@ -20,9 +23,11 @@ static void Check(bool value, int line) { #define Require(value) Check((value), __LINE__) constexpr std::size_t Count = 16; -using Nodes = std::array; +template +using Nodes = std::array; -static int Index(FT_ListNode node, const Nodes& nodes) { +template +static int Index(TNode* node, const Nodes& nodes) { if (!node) return -1; for (std::size_t i = 0; i < nodes.size(); ++i) if (node == &nodes[i]) return static_cast(i); Require(false); @@ -30,10 +35,10 @@ static int Index(FT_ListNode node, const Nodes& nodes) { } struct Lists { - FT_ListRec guest{}; + GuestListRec guest{}; FT_ListRec native{}; - Nodes actual{}; - Nodes expected{}; + Nodes actual{}; + Nodes expected{}; std::array values{11, 22, 33, 44}; Lists() { @@ -114,9 +119,10 @@ static void Mutations() { std::printf("list mutations: 2048 states compared with native FreeType\n"); } +template struct Walk { - FT_List list; - Nodes* nodes; + std::conditional_t, GuestListRec, FT_ListRec>* list; + Nodes* nodes; bool guest; bool remove; std::size_t stop; @@ -124,12 +130,13 @@ struct Walk { std::vector visits; }; -static int Visit(FT_ListNode node, void* user, bool guest) { - auto& state = *static_cast(user); +template +static int Visit(TNode* node, void* user, bool guest) { + auto& state = *static_cast*>(user); Require(state.guest == guest); state.visits.push_back(Index(node, *state.nodes)); if (state.remove) { - if (guest) FT_List_Remove_nid_postfix(state.list, node); + if constexpr (std::is_same_v) FT_List_Remove_nid_postfix(state.list, node); else FT_List_Remove(state.list, node); node->prev = nullptr; node->next = nullptr; @@ -172,8 +179,9 @@ struct Event { bool operator==(const Event&) const = default; }; +template struct Disposal { - FT_List list; + TList* list; FT_Memory native; GuestMemoryRec* guest; void* user; @@ -181,34 +189,36 @@ struct Disposal { std::vector events; }; -static void Release(Disposal& state, void* block) { +template +static void Release(Disposal& state, void* block) { const auto found = state.live.find(block); Require(found != state.live.end()); state.events.push_back({2, found->second, state.list->head == nullptr || state.list->tail == nullptr}); state.live.erase(found); - delete static_cast(block); + using Node = std::conditional_t, GuestListNodeRec, FT_ListNodeRec>; + delete static_cast(block); } static void NativeFree(FT_Memory memory, void* block) { - auto& state = *static_cast(memory->user); + auto& state = *static_cast*>(memory->user); Require(state.native == memory); Release(state, block); } static void APS5_VABI GuestFree(GuestMemoryRec* memory, void* block) { - auto& state = *static_cast(memory->user); + auto& state = *static_cast*>(memory->user); Require(state.guest == memory); Release(state, block); } static void NativeDestroy(FT_Memory memory, void* data, void* user) { - auto& state = *static_cast(memory->user); + auto& state = *static_cast*>(memory->user); Require(state.native == memory && state.user == user); state.events.push_back({1, data ? *static_cast(data) : -1, state.list->head == nullptr || state.list->tail == nullptr}); } static void APS5_VABI GuestDestroy(GuestMemoryRec* memory, void* data, void* user) { - auto& state = *static_cast(memory->user); + auto& state = *static_cast*>(memory->user); Require(state.guest == memory && state.user == user); state.events.push_back({1, data ? *static_cast(data) : -1, state.list->head == nullptr || state.list->tail == nullptr}); } @@ -216,7 +226,8 @@ static void APS5_VABI GuestDestroy(GuestMemoryRec* memory, void* data, void* use static void FinalizationCases() { for (const bool destroy : {false, true}) { for (const unsigned count : {0u, 1u, 8u}) { - FT_ListRec guestList{}, nativeList{}; + GuestListRec guestList{}; + FT_ListRec nativeList{}; std::array ids{0, 1, 2, 3, 4, 5, 6, 7}; int token = 9; Disposal guest{&guestList, nullptr, nullptr, &token, {}, {}}; @@ -226,7 +237,7 @@ static void FinalizationCases() { guest.guest = &guestMemory; native.native = &nativeMemory; for (unsigned i = 0; i < count; ++i) { - auto* a = new FT_ListNodeRec{}; + auto* a = new GuestListNodeRec{}; auto* b = new FT_ListNodeRec{}; a->data = b->data = i == 0 ? nullptr : &ids[i]; guest.live.emplace(a, i); diff --git a/core/libs/tests/GuestFreeTypeOutlineBorder.cpp b/core/libs/tests/GuestFreeTypeOutlineBorder.cpp new file mode 100644 index 0000000000..069142dbb8 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeOutlineBorder.cpp @@ -0,0 +1,274 @@ +#include "prx/libSceFreeType/include/Api.hpp" +#include +#include FT_FREETYPE_H +#include FT_STROKER_H +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + +using FreeType::GuestOutline; +using FreeType::GuestVector; +using Query = decltype(&FT_Outline_GetInsideBorder_nid_postfix); +static_assert(std::is_same_v); +static_assert(sizeof(GuestOutline) == 40 && alignof(GuestOutline) == 8); +static_assert(offsetof(GuestOutline, points) == 8 && offsetof(GuestOutline, contours) == 24); +static_assert(offsetof(GuestOutline, flags) == 32 && sizeof(GuestVector) == 16); +static_assert(sizeof(FT_StrokerBorder) == 4); + +namespace { + +bool nativeInside; + +void Require(bool result, const char* message) { + if (result) return; + std::fprintf(stderr, "FreeType outline border: %s\n", message); + std::exit(1); +} + +int APS5_VABI Native(const GuestOutline* input) { + if (!input) return nativeInside ? FT_Outline_GetInsideBorder(nullptr) : FT_Outline_GetOutsideBorder(nullptr); + FT_Outline outline{}; + outline.n_points = input->n_points; + outline.n_contours = input->n_contours; + outline.flags = input->flags; + std::vector points; + std::vector contours; + if (outline.n_points > 0) { + for (int i = 0; i < outline.n_points; ++i) { + Require(input->points[i].x >= std::numeric_limits::min() && input->points[i].x <= std::numeric_limits::max(), "reference x range"); + Require(input->points[i].y >= std::numeric_limits::min() && input->points[i].y <= std::numeric_limits::max(), "reference y range"); + points.push_back({static_cast(input->points[i].x), static_cast(input->points[i].y)}); + } + if (outline.n_contours > 0) + for (int i = 0; i < outline.n_contours; ++i) contours.push_back(input->contours[i]); + } + outline.points = points.data(); + outline.contours = contours.data(); + return nativeInside ? FT_Outline_GetInsideBorder(&outline) : FT_Outline_GetOutsideBorder(&outline); +} + +int Expected(bool inside, bool clockwise) { return inside == clockwise ? 1 : 0; } + +class ReadOnlyTail { + void* memory; + std::size_t page; + void* data; +public: + ReadOnlyTail(const void* source, std::size_t bytes) { +#ifdef _WIN32 + SYSTEM_INFO info; + GetSystemInfo(&info); + page = info.dwPageSize; + memory = VirtualAlloc(nullptr, page * 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + Require(memory != nullptr, "allocate guard pages"); +#else + page = static_cast(sysconf(_SC_PAGESIZE)); + memory = mmap(nullptr, page * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(memory != MAP_FAILED, "allocate guard pages"); +#endif + Require(bytes <= page, "guard fixture size"); + data = static_cast(memory) + page - bytes; + std::memcpy(data, source, bytes); +#ifdef _WIN32 + DWORD previous; + Require(VirtualProtect(memory, page, PAGE_READONLY, &previous) != 0, "read-only input"); + Require(VirtualProtect(static_cast(memory) + page, page, PAGE_NOACCESS, &previous) != 0, "inaccessible neighbor"); +#else + Require(mprotect(memory, page, PROT_READ) == 0, "read-only input"); + Require(mprotect(static_cast(memory) + page, page, PROT_NONE) == 0, "inaccessible neighbor"); +#endif + } + ~ReadOnlyTail() { +#ifdef _WIN32 + VirtualFree(memory, 0, MEM_RELEASE); +#else + munmap(memory, page * 2); +#endif + } + template T* As() const { return static_cast(data); } + void* End() const { return static_cast(memory) + page; } +}; + +void Cases(Query query, bool inside, bool guest) { + Require(query(nullptr) == Expected(inside, true), "null outline"); + GuestOutline outline{}; + outline.points = reinterpret_cast(std::uintptr_t{1}); + outline.contours = reinterpret_cast(std::uintptr_t{1}); + Require(query(&outline) == Expected(inside, true), "empty outline does not read points or contours"); + std::array points{{{0, 0}, {0, 64}, {64, 64}, {64, 0}}}; + std::uint16_t end = 3; + outline = {1, 4, points.data(), nullptr, &end, 0}; + Require(query(&outline) == Expected(inside, true), "clockwise polygon"); + const auto original = points; + for (const int flags : {0, 1, 2, -1}) { + outline.flags = flags; + Require(query(&outline) == Expected(inside, true), "orientation ignores outline flags"); + } + Require(std::memcmp(points.data(), original.data(), sizeof(points)) == 0, "input points unchanged"); + std::reverse(points.begin(), points.end()); + Require(query(&outline) == Expected(inside, false), "counterclockwise polygon"); + for (auto& point : points) point.y = 0; + Require(query(&outline) == Expected(inside, false), "collapsed polygon uses no-orientation fallback"); + points = original; + outline.n_contours = 0; + Require(query(&outline) == Expected(inside, false), "no contours"); + outline.n_contours = 1; + for (const std::int64_t edge : {std::int64_t{0x1000000}, std::int64_t{0x1000001}}) { + points = {{{-edge, -edge}, {-edge, edge}, {edge, edge}, {edge, -edge}}}; + Require(query(&outline) == (edge == 0x1000000 ? Expected(inside, true) : Expected(inside, false)), "coordinate-limit boundary"); + } + std::array mixed{}; + std::copy(original.begin(), original.end(), mixed.begin()); + std::copy(original.rbegin(), original.rend(), mixed.begin() + 4); + std::array mixedEnds{{3, 7}}; + GuestOutline cancelled{2, 8, mixed.data(), nullptr, mixedEnds.data(), 0}; + Require(query(&cancelled) == Expected(inside, false), "non-collapsed opposite contours cancel area"); + std::vector manyPoints(65535); + std::copy(original.rbegin(), original.rend(), manyPoints.begin()); + std::uint16_t manyEnd = 65534; + GuestOutline many{1, 65535, manyPoints.data(), nullptr, &manyEnd, 0}; + Require(query(&many) == Expected(inside, false), "unsigned point count above 32767"); + std::fill(manyPoints.begin(), manyPoints.end(), GuestVector{}); + std::copy(original.begin(), original.end(), manyPoints.end() - 4); + std::vector manyEnds(65532); + for (unsigned i = 0; i < manyEnds.size() - 1; ++i) manyEnds[i] = static_cast(i); + manyEnds.back() = 65534; + many.n_contours = 65532; + many.contours = manyEnds.data(); + Require(query(&many) == Expected(inside, true), "unsigned contour count above 32767"); + if (guest) { + for (const auto edge : {std::numeric_limits::min(), std::numeric_limits::max()}) { + points = original; + points[0].x = edge; + Require(query(&outline) == Expected(inside, false), "64-bit out-of-range coordinate"); + } + points = original; + std::array repeated{}; + std::array ends{}; + const std::array large{{{0, 0}, {0, 16384}, {16384, 16384}, {16384, 0}}}; + for (unsigned i = 0; i < 5; ++i) { + std::copy(large.begin(), large.end(), repeated.begin() + i * 4); + ends[i] = static_cast(i * 4 + 3); + } + GuestOutline wide{5, 20, repeated.data(), nullptr, ends.data(), 0}; + Require(query(&wide) == Expected(inside, true), "LP64 area must not wrap at 32 bits"); + ReadOnlyTail guardedPoints(original.data(), sizeof(original)); + ReadOnlyTail guardedEnds(&end, sizeof(end)); + GuestOutline guarded{1, 4, guardedPoints.As(), static_cast(guardedPoints.End()), guardedEnds.As(), -1}; + ReadOnlyTail guardedOutline(&guarded, sizeof(guarded)); + Require(query(guardedOutline.As()) == Expected(inside, true), "read-only bounded inputs and unused inaccessible tags"); + outline = {1, 4, nullptr, nullptr, &end, 0}; + bool threw = false; + try { query(&outline); } catch (const std::invalid_argument&) { threw = true; } + Require(threw, "null positive-count points are explicitly unsupported"); + outline.points = points.data(); + outline.contours = nullptr; + threw = false; + try { query(&outline); } catch (const std::invalid_argument&) { threw = true; } + Require(threw, "null positive-count contours are explicitly unsupported"); + for (const std::uint16_t badEnd : {std::uint16_t{4}, std::uint16_t{0xffff}}) { + auto invalidEnd = badEnd; + outline.contours = &invalidEnd; + threw = false; + try { query(&outline); } catch (const std::invalid_argument&) { threw = true; } + Require(threw, "invalid contour endpoint is explicitly unsupported"); + } + for (const std::uint16_t nextEnd : {std::uint16_t{2}, std::uint16_t{3}}) { + std::array invalidEnds{{3, nextEnd}}; + outline.n_contours = 2; + outline.contours = invalidEnds.data(); + threw = false; + try { query(&outline); } catch (const std::invalid_argument&) { threw = true; } + Require(threw, "non-increasing contour endpoints are explicitly unsupported"); + } + } + std::uint32_t state = 7; + for (unsigned iteration = 0; iteration < 512; ++iteration) { + for (auto& point : points) { + state = state * 1664525u + 1013904223u; + point.x = static_cast((state >> 16) & 255) - 128; + state = state * 1664525u + 1013904223u; + point.y = static_cast((state >> 16) & 255) - 128; + } + outline = {1, 4, points.data(), nullptr, &end, 0}; + nativeInside = inside; + Require(query(&outline) == Native(&outline), "bounded polygon matches bundled FreeType"); + } + if (guest) { + points = original; + outline = {1, 4, points.data(), nullptr, &end, 0}; + std::array threads; + for (auto& thread : threads) thread = std::thread([&] { + for (unsigned i = 0; i < 100; ++i) Require(query(&outline) == Expected(inside, true), "concurrent independent query"); + }); + for (auto& thread : threads) thread.join(); + } +} + +} + +int main(int argc, char** argv) { +#ifdef _WIN32 + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); +#endif + if (argc == 2 && std::strcmp(argv[1], "reference") == 0) { + for (const bool inside : {false, true}) { + nativeInside = inside; + Cases(Native, inside, false); + } + std::puts("Bundled FreeType reference cases passed"); + return 0; + } + Require(argc == 4, "expected library, border and export mode"); + const bool inside = std::strcmp(argv[2], "inside") == 0; + const bool patched = std::strcmp(argv[3], "patched") == 0; + const char* symbol = patched ? (inside ? "Pz262plj20w" : "sKneJjUXWkQ") : + (inside ? "FT_Outline_GetInsideBorder_nid_postfix" : "FT_Outline_GetOutsideBorder_nid_postfix"); +#ifdef _WIN32 + const auto library = LoadLibraryA(argv[1]); + Require(library != nullptr, "load requested library"); + const auto checkPath = [](HMODULE module, const std::filesystem::path& expected) { + Require(module != nullptr, "load matching runtime module"); + std::array path{}; + const auto length = GetModuleFileNameW(module, path.data(), static_cast(path.size())); + Require(length != 0 && length < path.size(), "read loaded module path"); + Require(std::filesystem::equivalent(expected, std::filesystem::path(path.data())), "loaded module variant path"); + }; + const auto requested = std::filesystem::absolute(argv[1]); + checkPath(library, requested); + checkPath(GetModuleHandleW(L"libc.prx"), requested.parent_path() / "libc.prx"); + auto query = reinterpret_cast(GetProcAddress(library, symbol)); +#else + const auto library = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL); + if (!library) std::fprintf(stderr, "%s\n", dlerror()); + Require(library != nullptr, "load requested library"); + auto query = reinterpret_cast(dlsym(library, symbol)); +#endif + if (!query) std::fprintf(stderr, "Missing %s\n", symbol); + Require(query != nullptr, "missing outline-border export"); + Cases(query, inside, true); + std::puts("Guest outline-border cases passed"); +#ifdef _WIN32 + FreeLibrary(library); +#else + dlclose(library); +#endif +} diff --git a/core/libs/tests/GuestFreeTypePfr.cpp b/core/libs/tests/GuestFreeTypePfr.cpp new file mode 100644 index 0000000000..a0a217c393 --- /dev/null +++ b/core/libs/tests/GuestFreeTypePfr.cpp @@ -0,0 +1,261 @@ +#include "FreeTypeFontFixture.hpp" +#include "prx/libSceFreeType/include/Api.hpp" +#include +#include +#include FT_FREETYPE_H +#include FT_PFR_H +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +using namespace FreeType; + +static void Check(bool value, int line) { + if (value) return; + std::fprintf(stderr, "PFR check failed at line %d\n", line); + std::exit(1); +} +#define Require(value) Check((value), __LINE__) + +static void CheckNullFace(int error) { + if (error != FT_Err_Invalid_Argument) std::fprintf(stderr, "PFR null-face returned %d, expected %d\n", error, FT_Err_Invalid_Argument); + Require(error == FT_Err_Invalid_Argument); +} + +static void Put24(std::vector& bytes, std::uint32_t value) { + bytes.push_back(static_cast(value >> 16)); + Put16(bytes, value); +} + +static void Field(std::vector& bytes, unsigned offset, unsigned width, std::uint32_t value) { + for (unsigned index = 0; index < width; ++index) bytes[offset + index] = static_cast(value >> (8 * (width - index - 1))); +} + +static std::vector PfrFont() { + std::vector physical; + PutAll16(physical, {1, 1000, 2000, 0, -200, 1000, 800}); + physical.push_back(0x84); + physical.insert(physical.end(), {1, 7, 4, 1, 0xFF, 0x9C, 0, 65, 66, 20}); + Put24(physical, 0); + physical.push_back(0); + physical.insert(physical.end(), {0, 0}); + PutAll16(physical, {0, 0, 2}); + for (const auto [code, advance] : {std::pair{65, 600}, std::pair{66, -120}}) { + physical.push_back(static_cast(code)); + Put16(physical, static_cast(advance)); + physical.push_back(3); + Put16(physical, 1); + } + std::vector bytes(58); + Field(bytes, 0, 4, 0x50465230); + Field(bytes, 4, 2, 4); + Field(bytes, 6, 2, 0x0D0A); + Field(bytes, 8, 2, 58); + Field(bytes, 10, 2, 7); + Field(bytes, 12, 2, 58); + Field(bytes, 14, 2, 18); + Field(bytes, 16, 3, 18); + Field(bytes, 19, 3, 65); + Field(bytes, 22, 2, static_cast(physical.size())); + Field(bytes, 24, 3, static_cast(physical.size())); + Field(bytes, 27, 3, 83); + Field(bytes, 30, 2, 3); + Field(bytes, 32, 3, 4); + Field(bytes, 35, 3, 83 + static_cast(physical.size())); + Field(bytes, 52, 2, 1); + Field(bytes, 56, 2, 2); + PutAll16(bytes, {1, 18}); + Put24(bytes, 65); + for (auto value : {0x10000u, 0u, 0u, 0x10000u}) Put24(bytes, value); + bytes.push_back(0); + Put16(bytes, static_cast(physical.size())); + Put24(bytes, 83); + bytes.insert(bytes.end(), physical.begin(), physical.end()); + bytes.insert(bytes.end(), 12, 0); + return bytes; +} + +template +struct Guarded { + std::array before{0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5}; + TValue value; + std::array after{0xD3, 0xD3, 0xD3, 0xD3, 0xD3, 0xD3, 0xD3, 0xD3}; + + void Verify() const { + for (auto byte : before) Require(byte == 0xA5); + for (auto byte : after) Require(byte == 0xD3); + } +}; + +static_assert(offsetof(Guarded, after) == offsetof(Guarded, value) + sizeof(std::uint32_t)); +static_assert(offsetof(Guarded, after) == offsetof(Guarded, value) + sizeof(std::int64_t)); +static_assert(offsetof(Guarded, after) == offsetof(Guarded, value) + sizeof(GuestVector)); + +static void Reference(const std::vector& font) { + FT_Library library = nullptr; + FT_Face face = nullptr; + Require(FT_Init_FreeType(&library) == 0); + Require(FT_New_Memory_Face(library, font.data(), static_cast(font.size()), 0, &face) == 0); + Require(face->num_glyphs == 3 && FT_Get_Char_Index(face, 65) == 1 && FT_Get_Char_Index(face, 66) == 2); + Require(FT_Set_Pixel_Sizes(face, 20, 30) == 0); + FT_UInt outline = 0, metrics = 0; + FT_Fixed x = 0, y = 0; + Require(FT_Get_PFR_Metrics(face, &outline, &metrics, &x, &y) == 0); + Require(outline == 1000 && metrics == 2000 && x == FT_DivFix(20 * 64, 2000) && y == FT_DivFix(30 * 64, 2000)); + FT_Pos advance = 0; + Require(FT_Get_PFR_Advance(face, 1, &advance) == 0 && advance == 600); + Require(FT_Get_PFR_Advance(face, 2, &advance) == 0 && advance == -120); + FT_Vector kerning{}; + Require(FT_Get_PFR_Kerning(face, 1, 2, &kerning) == 0 && kerning.x == -80 && kerning.y == 0); + Require(FT_Get_Kerning(face, 1, 2, FT_KERNING_UNSCALED, &kerning) == 0 && kerning.x == -40); + Require(FT_Load_Glyph(face, 1, FT_LOAD_NO_SCALE) == 0); + Require(FT_Done_Face(face) == 0 && FT_Done_FreeType(library) == 0); +} + +#ifdef _WIN32 +using Module = HMODULE; +#else +using Module = void*; +#endif + +static Module Open(const std::filesystem::path& path) { +#ifdef _WIN32 + const auto module = LoadLibraryExW(path.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); + Require(module != nullptr); + std::array selected{}; + const auto length = GetModuleFileNameW(module, selected.data(), static_cast(selected.size())); + Require(length != 0 && length < selected.size()); + Require(std::filesystem::equivalent(path, std::filesystem::path(selected.data()))); +#else + const auto module = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); + if (!module) std::fprintf(stderr, "%s\n", dlerror()); + Require(module != nullptr); +#endif + return module; +} + +template +static TFunction Resolve(Module module, const std::string& name, bool patched) { + const auto symbol = patched ? Nid::ComputeNid(name, "") : name + "_nid_postfix"; +#ifdef _WIN32 + const auto address = GetProcAddress(module, symbol.c_str()); +#else + const auto address = dlsym(module, symbol.c_str()); +#endif + if (!address) std::fprintf(stderr, "Missing %s (%s)\n", name.c_str(), symbol.c_str()); + Require(address != nullptr); + return reinterpret_cast(address); +} + +using Metrics = decltype(&FT_Get_PFR_Metrics_nid_postfix); +using Advance = decltype(&FT_Get_PFR_Advance_nid_postfix); +using Kerning = decltype(&FT_Get_PFR_Kerning_nid_postfix); + +static void MetricsCases(Metrics call, GuestFaceRec* face, bool pfr) { + Guarded outline{.value = 0xDEADBEEFu}, metrics{.value = 0xDEADBEEFu}; + Guarded x{.value = 0x123456789ABCll}, y{.value = 0x123456789ABCll}; + for (unsigned mask = 0; mask < 16; ++mask) { + CheckNullFace(call(nullptr, mask & 1 ? &outline.value : nullptr, mask & 2 ? &metrics.value : nullptr, + mask & 4 ? &x.value : nullptr, mask & 8 ? &y.value : nullptr)); + Require(outline.value == 0xDEADBEEFu && metrics.value == 0xDEADBEEFu && x.value == 0x123456789ABCll && y.value == 0x123456789ABCll); + outline.Verify(); metrics.Verify(); x.Verify(); y.Verify(); + } + const auto expectedX = pfr ? FT_DivFix(20 * 64, 2000) : face->size->metrics.x_scale; + const auto expectedY = pfr ? FT_DivFix(30 * 64, 2000) : face->size->metrics.y_scale; + for (unsigned mask = 0; mask < 16; ++mask) { + outline.value = metrics.value = 0xDEADBEEFu; + x.value = y.value = 0x123456789ABCll; + Require(call(face, mask & 1 ? &outline.value : nullptr, mask & 2 ? &metrics.value : nullptr, + mask & 4 ? &x.value : nullptr, mask & 8 ? &y.value : nullptr) == (pfr ? 0 : FT_Err_Unknown_File_Format)); + Require(outline.value == (mask & 1 ? 1000u : 0xDEADBEEFu)); + Require(metrics.value == (mask & 2 ? pfr ? 2000u : 1000u : 0xDEADBEEFu)); + Require(x.value == (mask & 4 ? expectedX : 0x123456789ABCll)); + Require(y.value == (mask & 8 ? expectedY : 0x123456789ABCll)); + outline.Verify(); metrics.Verify(); x.Verify(); y.Verify(); + } + Require(call(face, &outline.value, &outline.value, &x.value, &x.value) == (pfr ? 0 : FT_Err_Unknown_File_Format)); + Require(outline.value == (pfr ? 2000u : 1000u) && x.value == expectedY); +} + +static void AdvanceCases(Advance call, GuestFaceRec* face, bool pfr) { + Guarded value{.value = 0x123456789ABCll}; + CheckNullFace(call(nullptr, 1, nullptr)); + CheckNullFace(call(nullptr, 1, &value.value)); + Require(value.value == 0x123456789ABCll); + value.Verify(); + Require(call(face, 1, nullptr) == FT_Err_Invalid_Argument); + for (const auto index : {0u, 1u, 2u, 3u, std::numeric_limits::max()}) { + value.value = 0x123456789ABCll; + const auto error = call(face, index, &value.value); + Require(error == (pfr && (index == 1 || index == 2) ? 0 : FT_Err_Invalid_Argument)); + Require(value.value == (pfr ? index == 1 ? 600 : index == 2 ? -120 : 0 : 0x123456789ABCll)); + value.Verify(); + } +} + +static void KerningCases(Kerning call, GuestFaceRec* face, bool pfr) { + Guarded value{.value = {0x123456789ABCll, -0x123456789ABCll}}; + CheckNullFace(call(nullptr, 1, 2, nullptr)); + CheckNullFace(call(nullptr, 1, 2, &value.value)); + Require(value.value.x == 0x123456789ABCll && value.value.y == -0x123456789ABCll); + value.Verify(); + Require(call(face, 1, 2, nullptr) == FT_Err_Invalid_Argument); + for (const auto [left, right] : {std::pair{1u, 2u}, std::pair{2u, 1u}, std::pair{0u, 2u}, std::pair{0xFFFFFFFFu, 1u}}) { + value.value = {0x123456789ABCll, -0x123456789ABCll}; + Require(call(face, left, right, &value.value) == 0); + Require(value.value.x == (pfr && left == 1 && right == 2 ? -80 : 0) && value.value.y == 0); + value.Verify(); + } +} + +int main(int argc, char** argv) { + const auto pfrFont = PfrFont(); + Reference(pfrFont); + if (argc == 2 && std::string(argv[1]) == "reference") return 0; + Require(argc == 4); + const auto directory = std::filesystem::absolute(argv[1]); + const bool patched = std::string(argv[2]) == "patched"; + const std::string operation = argv[3]; + Require(operation == "metrics" || operation == "advance" || operation == "kerning"); + const auto base = Open(directory / "libSceFreeType.prx"); + const auto optional = Open(directory / "libSceFreeTypeOptOl.prx"); + const auto init = Resolve(base, "FT_Init_FreeType", patched); + const auto done = Resolve(base, "FT_Done_FreeType", patched); + const auto create = Resolve(base, "FT_New_Memory_Face", patched); + const auto destroy = Resolve(base, "FT_Done_Face", patched); + const auto resize = Resolve(base, "FT_Set_Pixel_Sizes", patched); + Metrics metrics = nullptr; + Advance advance = nullptr; + Kerning kerning = nullptr; + if (operation == "metrics") metrics = Resolve(optional, "FT_Get_PFR_Metrics", patched); + if (operation == "advance") advance = Resolve(optional, "FT_Get_PFR_Advance", patched); + if (operation == "kerning") kerning = Resolve(optional, "FT_Get_PFR_Kerning", patched); + void* library = nullptr; + Require(init(&library) == 0); + const auto ttfFont = SquareGlyphFont(65, 700); + for (bool pfr : {true, false, true}) { + const auto& bytes = pfr ? pfrFont : ttfFont; + GuestFaceRec* face = nullptr; + Require(create(library, bytes.data(), static_cast(bytes.size()), 0, &face) == 0); + Require(resize(face, 20, 30) == 0); + std::array snapshot; + std::memcpy(snapshot.data(), face, snapshot.size()); + if (metrics) MetricsCases(metrics, face, pfr); + if (advance) AdvanceCases(advance, face, pfr); + if (kerning) KerningCases(kerning, face, pfr); + Require(std::memcmp(snapshot.data(), face, snapshot.size()) == 0); + Require(destroy(face) == 0); + } + Require(done(library) == 0); + std::printf("PFR %s %s: positive/non-PFR, LP64, bounds, errors, optional outputs and lifetime passed\n", operation.c_str(), argv[2]); +} diff --git a/core/libs/tests/GuestFreeTypePfrNull.cpp b/core/libs/tests/GuestFreeTypePfrNull.cpp new file mode 100644 index 0000000000..bfde675ce7 --- /dev/null +++ b/core/libs/tests/GuestFreeTypePfrNull.cpp @@ -0,0 +1,95 @@ +#include "prx/libSceFreeType/include/Api.hpp" +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include FT_FONT_FORMATS_H +#include FT_PFR_H +#include +#include +#include +#include + +namespace { + +unsigned imports = 0; + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "PFR null face: %s\n", message); + std::exit(1); +} + +void CheckError(int result, const char* operation) { + if (result == 6) return; + std::fprintf(stderr, "PFR null face: %s returned %d, expected 6; host imports %u\n", operation, result, imports); + std::exit(1); +} + +template +struct Guarded { + std::uint64_t before = UINT64_C(0x123456789abcdef0); + T value; + std::uint64_t after = UINT64_C(0xfedcba9876543210); + + void Verify() const { + Require(before == UINT64_C(0x123456789abcdef0) && after == UINT64_C(0xfedcba9876543210), "output canaries"); + } +}; + +} + +namespace FreeType { + +GuestFace* FaceOf(const GuestFaceRec* face) { + Require(face == nullptr, "fixture received a nonnull face"); + return nullptr; +} + +} + +extern "C" { + +FT_Error FT_Get_PFR_Advance(FT_Face, FT_UInt, FT_Pos*) { + ++imports; + return FT_Err_Invalid_Face_Handle; +} + +FT_Error FT_Get_PFR_Kerning(FT_Face, FT_UInt, FT_UInt, FT_Vector*) { + ++imports; + return FT_Err_Invalid_Face_Handle; +} + +FT_Error FT_Get_PFR_Metrics(FT_Face, FT_UInt*, FT_UInt*, FT_Fixed*, FT_Fixed*) { + ++imports; + return FT_Err_Invalid_Face_Handle; +} + +const char* FT_Get_Font_Format(FT_Face) { + ++imports; + return nullptr; +} + +} + +int main() { + Guarded outline{.value = 0xdeadbeefu}, metrics{.value = 0xa5a5a5a5u}; + Guarded x{.value = INT64_C(0x123456789abc)}, y{.value = -INT64_C(0x123456789abc)}; + for (unsigned mask = 0; mask < 16; ++mask) { + CheckError(FT_Get_PFR_Metrics_nid_postfix(nullptr, mask & 1 ? &outline.value : nullptr, + mask & 2 ? &metrics.value : nullptr, mask & 4 ? &x.value : nullptr, mask & 8 ? &y.value : nullptr), "metrics"); + Require(outline.value == 0xdeadbeefu && metrics.value == 0xa5a5a5a5u && + x.value == INT64_C(0x123456789abc) && y.value == -INT64_C(0x123456789abc), "metrics output unchanged"); + outline.Verify(); metrics.Verify(); x.Verify(); y.Verify(); + } + Guarded advance{.value = INT64_C(0x123456789abc)}; + Guarded vector{.value = {INT64_C(0x123456789abc), -INT64_C(0x123456789abc)}}; + for (const auto index : std::array{0, 1, UINT32_MAX}) { + for (const bool output : {false, true}) { + CheckError(FT_Get_PFR_Advance_nid_postfix(nullptr, index, output ? &advance.value : nullptr), "advance"); + CheckError(FT_Get_PFR_Kerning_nid_postfix(nullptr, index, UINT32_MAX - index, output ? &vector.value : nullptr), "kerning"); + Require(advance.value == INT64_C(0x123456789abc), "advance output unchanged"); + Require(vector.value.x == INT64_C(0x123456789abc) && vector.value.y == -INT64_C(0x123456789abc), "kerning output unchanged"); + advance.Verify(); vector.Verify(); + } + } + Require(imports == 0, "null face called a host import"); + std::puts("PFR null-face contract passed without host imports"); +} diff --git a/core/libs/tests/GuestFreebsdClocks.cpp b/core/libs/tests/GuestFreebsdClocks.cpp index 1ae0f71258..31673f8019 100644 --- a/core/libs/tests/GuestFreebsdClocks.cpp +++ b/core/libs/tests/GuestFreebsdClocks.cpp @@ -290,6 +290,23 @@ static void RequireZone(const char* zone, std::int64_t utc, std::int64_t standar Require(state.t == local && state.west_sec == universal.west_sec && state.dst_sec == universal.dst_sec); } +static void RequireTimezone(const char* zone, std::int32_t minutesEast, std::int32_t daylightMinutes) { + SetTimeZone(zone); + KernelTimezone tz{-1, -1}; + Require(sceKernelGettimezone(&tz) == SCE_OK); + Require(tz.tz_minuteswest == minutesEast && tz.tz_dsttime == RuntimeDaylight(daylightMinutes * 60) / 60); +} + +static void TimezoneReportsTheStandardOffsetAndDaylightAmount() { + RequireTimezone("UTC0", 0, 0); + RequireTimezone("IST-5:30", 330, 0); + RequireTimezone("BRT+3", -180, 0); + RequireTimezone("EST5EDT", -300, 60); + RequireTimezone("CET-1CEST", 60, 60); + Require(sceKernelGettimezone(nullptr) == static_cast(0x8002000e)); + SetTimeZone("UTC0"); +} + static void CalendarConversionsFollowTheHostZone() { constexpr std::int64_t winter = 1768478400; constexpr std::int64_t summer = 1782907200; @@ -308,6 +325,7 @@ int main() { SettimeValidatesLikeFreebsd(); CalendarConversionsInitializeTheirState(); CalendarConversionsFollowTheHostZone(); + TimezoneReportsTheStandardOffsetAndDaylightAmount(); SecondClockReportsWholeSeconds(); KernelTimezone zone{-1, -1}; Require(gettimeofday_nid_postfix(nullptr, nullptr) == SCE_OK); diff --git a/core/libs/tests/GuestFstatFault.cpp b/core/libs/tests/GuestFstatFault.cpp index 08ff17cd37..669cb3c903 100644 --- a/core/libs/tests/GuestFstatFault.cpp +++ b/core/libs/tests/GuestFstatFault.cpp @@ -6,13 +6,30 @@ #include "prx/libkernel/File/include/FileFlags.hpp" #include +#include #include #include #include #include #include +#include #include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif +#ifdef __linux__ +#include +#include +#include +#include +#include +#endif + static void Check(bool value, int line) { if (!value) { std::fprintf(stderr, "fstat check failed at line %d\n", line); @@ -31,6 +48,72 @@ static void RequireFailure(int descriptor, int error) { Require(sceKernelFstat(descriptor, nullptr) == static_cast(0x80020000u | static_cast(error))); } +static void RequireBadPointerAt(int descriptor, const char* path, void* address) { + { + auto* bad = static_cast(address); + *__error_nid_postfix() = 0; + Require(fstat_nid_disambig1_nid_postfix(descriptor, bad) == -1 && *__error_nid_postfix() == GuestEfault); + Require(sceKernelFstat(descriptor, bad) == static_cast(0x80020000u | GuestEfault)); + *__error_nid_postfix() = 0; + Require(stat_nid_postfix(path, bad) == -1 && *__error_nid_postfix() == GuestEfault); + *__error_nid_postfix() = 0; + Require(lstat_nid_postfix(path, bad) == -1 && *__error_nid_postfix() == GuestEfault); + Require(sceKernelStat(path, bad) == static_cast(0x80020000u | GuestEfault)); + *__error_nid_postfix() = 0; + Require(_fstatfs_nid_postfix(descriptor, reinterpret_cast(bad)) == -1 && *__error_nid_postfix() == GuestEfault); + } +} + +static void RequireUnwritablePage(int descriptor, const char* path) { +#ifdef _WIN32 + void* page = VirtualAlloc(nullptr, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_READONLY); + Require(page != nullptr); + void* none = VirtualAlloc(nullptr, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_NOACCESS); + Require(none != nullptr); + RequireBadPointerAt(descriptor, path, page); + RequireBadPointerAt(descriptor, path, none); + VirtualFree(page, 0, MEM_RELEASE); + VirtualFree(none, 0, MEM_RELEASE); +#else + void* page = mmap(nullptr, 4096, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(page != MAP_FAILED); + void* none = mmap(nullptr, 4096, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(none != MAP_FAILED); + RequireBadPointerAt(descriptor, path, page); + RequireBadPointerAt(descriptor, path, none); + munmap(page, 4096); + munmap(none, 4096); +#endif +} + +#ifdef __linux__ +static void RequireNonEfaultFailureThrows() { + const pid_t child = fork(); + Require(child >= 0); + if (child == 0) { + sock_filter filter[] = { + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_process_vm_writev, 0, 1), + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM), + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), + }; + sock_fprog program{static_cast(sizeof(filter) / sizeof(filter[0])), filter}; + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0 || prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &program) != 0) _exit(2); + char source[8] = {1, 2, 3, 4, 5, 6, 7, 8}; + char destination[8] = {}; + try { + GuestCopyOut_nid_no_patch(destination, source, sizeof(source)); + } catch (const std::runtime_error&) { + _exit(destination[0] == 0 ? 0 : 3); + } + _exit(1); + } + int status = 0; + Require(waitpid(child, &status, 0) == child); + Require(WIFEXITED(status) && WEXITSTATUS(status) == 0); +} +#endif + int main() { const auto root = std::filesystem::temp_directory_path() / ("anyps5-fstat-fault-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); @@ -47,6 +130,12 @@ int main() { FileStat status{}; Require(fstat_nid_disambig1_nid_postfix(file, &status) == 0 && status.st_size == 5); RequireFailure(file, GuestEfault); + for (const std::uintptr_t address : {std::uintptr_t{1}, ~std::uintptr_t{0xfff}}) + RequireBadPointerAt(file, "/anyps5-fstat-fault-test/file.txt", reinterpret_cast(address)); + RequireUnwritablePage(file, "/anyps5-fstat-fault-test/file.txt"); +#ifdef __linux__ + RequireNonEfaultFailureThrows(); +#endif int sockets[2]; Require(socketpair_nid_postfix(1, 1, 0, sockets) == 0); diff --git a/core/libs/tests/GuestGameUpdate.cpp b/core/libs/tests/GuestGameUpdate.cpp index b3df072055..34c5c5308b 100644 --- a/core/libs/tests/GuestGameUpdate.cpp +++ b/core/libs/tests/GuestGameUpdate.cpp @@ -58,6 +58,42 @@ static void TestRequestArguments(int notInitialized, int invalidArgument, int in Require(sceGameUpdateTerminate() == 0); } +static void TestRequestLifecycle(int notFound) { + constexpr int tooManyRequests = static_cast(0x80412806); + constexpr int aborted = static_cast(0x80412807); + GameUpdateCheckParam param{}; + param.size = sizeof(param); + GameUpdateCheckResult result{}; + std::memset(&result, 0xAB, sizeof(result)); + result.size = sizeof(result); + const auto original = result; + + int ids[8]; + for (int& id : ids) { + id = sceGameUpdateCreateRequest(); + Require(id >= 0x20000000 && id <= 0x2FFFFFFF); + } + Require(ids[0] != ids[1]); + Require(sceGameUpdateCreateRequest() == tooManyRequests); + + Require(sceGameUpdateCheck(ids[0], ¶m, &result) == 0); + Require(!result.found && !result.addcont_found); + Require(std::memcmp(&result, &original, 8) == 0); + Require(std::memcmp(reinterpret_cast(&result) + 10, reinterpret_cast(&original) + 10, sizeof(result) - 10) == 0); + + Require(sceGameUpdateAbortRequest(ids[1]) == 0); + Require(sceGameUpdateCheck(ids[1], ¶m, &result) == aborted); + + Require(sceGameUpdateDeleteRequest(ids[3]) == 0); + Require(sceGameUpdateDeleteRequest(ids[3]) == notFound); + Require(sceGameUpdateCheck(ids[3], ¶m, &result) == notFound); + Require(sceGameUpdateCreateRequest() >= 0x20000000); + + Require(sceGameUpdateTerminate() == 0); + Require(sceGameUpdateInitialize() == 0); + Require(sceGameUpdateCheck(ids[0], ¶m, &result) == notFound); +} + int main() { constexpr int notInitialized = static_cast(0x80412801); constexpr int invalidArgument = static_cast(0x80412803); @@ -91,7 +127,7 @@ int main() { const auto original = info; RequireThrows([&] { sceGameUpdateGetAddcontLatestVersion(0, &label, &info); }); Require(std::memcmp(&info, &original, sizeof(info)) == 0); - RequireThrows([] { sceGameUpdateCreateRequest(); }); + TestRequestLifecycle(notFound); Require(sceGameUpdateTerminate() == 0); Require(sceGameUpdateTerminate() == notInitialized); diff --git a/core/libs/tests/GuestGetlogin.cpp b/core/libs/tests/GuestGetlogin.cpp index 385c37ce1a..5071c786c9 100644 --- a/core/libs/tests/GuestGetlogin.cpp +++ b/core/libs/tests/GuestGetlogin.cpp @@ -1,4 +1,5 @@ #include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel_sys/include/Api.hpp" #include "prx/libc/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include @@ -15,6 +16,7 @@ #include #else #include +#include #include #endif @@ -42,6 +44,27 @@ static std::string HostLogin() { #endif } +static char* GuardedPageEnd() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + const std::size_t pageSize = info.dwPageSize; + auto* memory = static_cast(VirtualAlloc(nullptr, pageSize * 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(memory != nullptr); + DWORD previous; + Require(VirtualProtect(memory + pageSize, pageSize, PAGE_NOACCESS, &previous) != 0); +#else + const long nativePageSize = sysconf(_SC_PAGESIZE); + Require(nativePageSize > 0); + const std::size_t pageSize = static_cast(nativePageSize); + void* allocation = mmap(nullptr, pageSize * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(allocation != MAP_FAILED); + auto* memory = static_cast(allocation); + Require(mprotect(memory + pageSize, pageSize, PROT_NONE) == 0); +#endif + return memory + pageSize; +} + static void RequireLogin(const std::string& expected) { std::vector output(expected.size() + 3, '\x5A'); *__error_nid_postfix() = 12345; @@ -77,4 +100,30 @@ int main() { for (int i = 0; i != 20; ++i) RequireLogin(expected); }); for (auto& reader : readers) reader.join(); + *__error_nid_postfix() = 0; + Require(setlogin_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == 14); + *__error_nid_postfix() = 0; + Require(setlogin_nid_postfix(reinterpret_cast(0x10)) == -1 && *__error_nid_postfix() == 14); + RequireLogin(expected); + const std::string longest(32, 'n'); + const std::string tooLong(33, 'n'); + *__error_nid_postfix() = 0; + Require(setlogin_nid_postfix(tooLong.c_str()) == -1 && *__error_nid_postfix() == 22); + RequireLogin(expected); + char* const pageEnd = GuardedPageEnd(); + std::memset(pageEnd - 33, 'p', 33); + *__error_nid_postfix() = 0; + Require(setlogin_nid_postfix(pageEnd - 33) == -1 && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(setlogin_nid_postfix(pageEnd - 32) == -1 && *__error_nid_postfix() == 14); + RequireLogin(expected); + pageEnd[-1] = '\0'; + Require(setlogin_nid_postfix(pageEnd - 33) == 0); + RequireLogin(std::string(32, 'p')); + Require(setlogin_nid_postfix("guest") == 0); + RequireLogin("guest"); + Require(setlogin_nid_postfix(longest.c_str()) == 0); + RequireLogin(longest); + Require(setlogin_nid_postfix("") == 0); + RequireLogin(""); } diff --git a/core/libs/tests/GuestGetrusage.cpp b/core/libs/tests/GuestGetrusage.cpp index 1cfd2b9fe8..5c37bcc0b6 100644 --- a/core/libs/tests/GuestGetrusage.cpp +++ b/core/libs/tests/GuestGetrusage.cpp @@ -41,6 +41,11 @@ int main() { RequireFailure(0, nullptr, 14); RequireFailure(1, nullptr, 14); RequireFailure(-1, nullptr, 14); + auto* const invalid = reinterpret_cast(static_cast(1)); + RequireFailure(2, invalid, 22); + RequireFailure(0, invalid, 14); + RequireFailure(1, invalid, 14); + RequireFailure(-1, invalid, 14); GuestResourceUsage children; std::memset(&children, 0xff, sizeof(children)); Require(getrusage_nid_postfix(-1, &children) == 0); diff --git a/core/libs/tests/GuestHeapReportForGame.cpp b/core/libs/tests/GuestHeapReportForGame.cpp index db56fe1a2c..ccc355a05d 100644 --- a/core/libs/tests/GuestHeapReportForGame.cpp +++ b/core/libs/tests/GuestHeapReportForGame.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/Api.hpp" +#include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -58,10 +59,20 @@ int main() { threw = true; } Require(threw, "null heap accepted"); + std::fflush(stdout); + const bool closed = _Stdout_nid_postfix.Close() == 0; + bool closedThrew = false; + try { + sceLibcInternalHeapErrorReportForGame_nid_postfix(heap, &block, 1); + } catch (const std::exception&) { + closedThrew = true; + } + if (!closed || closedThrew) { + std::fputs("heap error report on a closed standard output\n", stderr); + std::abort(); + } std::fflush(stderr); std::fclose(stderr); - std::fflush(stdout); - std::fclose(stdout); const std::string report = ReadAll(path); const std::string output = ReadAll(outputPath); diff --git a/core/libs/tests/GuestHmd.cpp b/core/libs/tests/GuestHmd.cpp index 257e9f381c..fa06463949 100644 --- a/core/libs/tests/GuestHmd.cpp +++ b/core/libs/tests/GuestHmd.cpp @@ -1,5 +1,6 @@ #include "ProcessParametersFixture.hpp" #include "prx/libSceHmd/Hmd.hpp" +#include "prx/libSceHmd/include/Api.hpp" #include #include @@ -50,7 +51,9 @@ void CheckDisconnected() { void CheckBufferQueries() { Require(sceHmdDistortionGetWorkMemoryAlign() == 0x400, "distortion work memory alignment"); + Require(sceHmdGetDistortionWorkMemoryAlign() == 0x400, "distortion work memory alignment query"); Require(sceHmdDistortionGetWorkMemorySize() == 0x20000, "distortion work memory size"); + Require(sceHmdGetDistortionWorkMemorySize() == 0x20000, "distortion work memory size query"); Require(sceHmdReprojectionQueryGarlicBuffAlign() == 0x100, "reprojection garlic buffer alignment"); Require(sceHmdReprojectionQueryGarlicBuffSize() == 0x100000, "reprojection garlic buffer size"); Require(sceHmdReprojectionQueryOnionBuffAlign() == 0x100, "reprojection onion buffer alignment"); diff --git a/core/libs/tests/GuestHostname.cpp b/core/libs/tests/GuestHostname.cpp index 9b6e788e19..871c2b68d1 100644 --- a/core/libs/tests/GuestHostname.cpp +++ b/core/libs/tests/GuestHostname.cpp @@ -18,7 +18,7 @@ static void Require(bool condition, int line) { #define Check(value) Require((value), __LINE__) int main() { - constexpr std::string_view expected = "AnyPS5"; + constexpr std::string_view expected = ""; for (std::size_t length = 0; length <= expected.size() + 2; ++length) { std::vector buffer(length + 2, '#'); *__error_nid_postfix() = 71; diff --git a/core/libs/tests/GuestHttp2.cpp b/core/libs/tests/GuestHttp2.cpp index 286b03b042..e58e84e291 100644 --- a/core/libs/tests/GuestHttp2.cpp +++ b/core/libs/tests/GuestHttp2.cpp @@ -4,211 +4,82 @@ #include #include #include +#include static void Require(bool value) { if (!value) std::abort(); } static constexpr int InvalidId = static_cast(0x817B1100); -static void RequireSslSetters(int id, int expected) { - Require(sceHttp2SetMinSslVersion(id, 0) == expected); - Require(sceHttp2SslEnableOption(id, 0) == expected); - Require(sceHttp2SslDisableOption(id, 0) == expected); -} - template static bool Throws(TCall call) { - try { - call(); - } catch (const std::exception&) { - return true; - } + try { call(); } catch (const std::exception&) { return true; } return false; } int main() { - Require(sceHttp2CreateCookieBox(0) == static_cast(0x817b121du)); Require(sceHttp2CreateCookieBox(0xffff) == static_cast(0x817b121du)); - const int independentBox = sceHttp2CreateCookieBox(0x10000); - Require(independentBox > 0); - Require(sceHttp2DeleteCookieBox(independentBox) == 0); - const int context = sceHttp2Init(1, 1, 0x10000, 4); - Require(context > 0); - Http2MemoryPoolStats stats{1, 1, 1, 1}; - Require(sceHttp2GetMemoryPoolStats(context, &stats) == 0); - Require(stats.pool_size == 0x10000 && stats.max_inuse_size == 0 && stats.current_inuse_size == 0 && stats.reserved == 0); const int box = sceHttp2CreateCookieBox(0x10000); - const int other = sceHttp2CreateCookieBox(0x10000); - Require(box > 0 && other > 0 && box != other && box != context); - const int discarded = sceHttp2CreateCookieBox(0x10000); - Require(discarded > 0 && discarded != box && discarded != other); - Require(sceHttp2DeleteCookieBox(discarded) == 0); - Require(sceHttp2DeleteCookieBox(discarded) == InvalidId); - Require(sceHttp2DeleteCookieBox(context) == InvalidId); - const int tmpl = sceHttp2CreateTemplate(context, "agent", 2, 0); - Require(tmpl > 0 && tmpl != box && tmpl != other); - Require(sceHttp2SetResolveRetry(tmpl, 3) == 0); - Require(sceHttp2SetCookieBox(tmpl, box) == 0); + const int context = sceHttp2Init(1, 1, 0x10000, 4); + const int tmpl = sceHttp2CreateTemplate(context, "agent", 3, 0); + const int proxyTemplate = sceHttp2CreateTemplate(context, "agent", 3, 1); + Require(proxyTemplate > 0 && sceHttp2DeleteTemplate(proxyTemplate) == 0); + Require(Throws([&] { sceHttp2CreateTemplate(context, "agent", 3, 2); })); + Require(Throws([&] { sceHttp2CreateTemplate(context, "agent", 3, -1); })); + const int request = sceHttp2CreateRequestWithURL(tmpl, "GET", "https://localhost/", 0); + Require(box > 0 && context > 0 && tmpl > 0 && request > 0); Require(sceHttp2SetCookieBox(tmpl, 0) == 0); - const int request = sceHttp2CreateRequestWithURL(tmpl, "POST", "https://example.com/", 16); - Require(request > 0); - Require(sceHttp2SetCookieBox(request, other) == 0); + Require(Throws([&] { sceHttp2SetCookieBox(tmpl, box); })); + Require(Throws([&] { sceHttp2CookieFlush(box); })); + Require(sceHttp2SetResolveRetry(tmpl, 0) == 0); + Require(Throws([&] { sceHttp2SetResolveRetry(tmpl, 3); })); Require(sceHttp2SetRequestNoContentLength(request) == 0); + Require(Throws([&] { sceHttp2AddRequestHeader(request, "bad:name", "value", 0); })); + Require(Throws([&] { sceHttp2AddRequestHeader(request, "x-header", "value\r\ninjected: yes", 0); })); + Require(Throws([&] { sceHttp2AddRequestHeader(request, "connection", "keep-alive", 0); })); Require(Throws([] { sceHttp2WebSocketSetPingInterval(0, 1000000); })); - Require(sceHttp2CookieFlush(context) == 0); - Require(sceHttp2SendRequest(request, nullptr, 0) == static_cast(0x80436063)); - - RequireSslSetters(0, InvalidId); - RequireSslSetters(-1, InvalidId); - RequireSslSetters(0x7fffffff, InvalidId); - RequireSslSetters(context, 0); - RequireSslSetters(box, 0); - RequireSslSetters(other, 0); - RequireSslSetters(tmpl, 0); - RequireSslSetters(request, 0); - - const int doomedRequest = sceHttp2CreateRequestWithURL(tmpl, "GET", "https://example.com/doomed", 0); - Require(doomedRequest > 0); - Require(sceHttp2DeleteRequest(doomedRequest) == 0); - RequireSslSetters(doomedRequest, InvalidId); - Require(sceHttp2DeleteRequest(doomedRequest) == InvalidId); - - const int doomedTemplate = sceHttp2CreateTemplate(context, "agent", 2, 0); - Require(doomedTemplate > 0); - const int doomedTemplateRequest = sceHttp2CreateRequestWithURL(doomedTemplate, "GET", "https://example.com/child", 0); - Require(doomedTemplateRequest > 0); - Require(sceHttp2DeleteTemplate(doomedTemplate) == 0); - RequireSslSetters(doomedTemplate, InvalidId); - RequireSslSetters(doomedTemplateRequest, InvalidId); - Require(sceHttp2DeleteTemplate(doomedTemplate) == InvalidId); - - const int doomedContext = sceHttp2Init(1, 1, 0x2000, 4); - Require(doomedContext > 0); - const int doomedContextBox = sceHttp2CreateCookieBox(0x10001); - const int doomedContextTemplate = sceHttp2CreateTemplate(doomedContext, "agent", 2, 0); - Require(doomedContextBox > 0 && doomedContextTemplate > 0); - const int doomedContextRequest = sceHttp2CreateRequestWithURL(doomedContextTemplate, "GET", "https://example.com/descendant", 0); - Require(doomedContextRequest > 0); - RequireSslSetters(doomedContext, 0); - Require(sceHttp2Term(doomedContext) == 0); - RequireSslSetters(doomedContext, InvalidId); - RequireSslSetters(doomedContextBox, 0); - Require(sceHttp2DeleteCookieBox(doomedContextBox) == 0); - Require(sceHttp2DeleteCookieBox(doomedContextBox) == InvalidId); - RequireSslSetters(doomedContextTemplate, InvalidId); - RequireSslSetters(doomedContextRequest, InvalidId); - Require(sceHttp2Term(doomedContext) == InvalidId); - - const int freshContext = sceHttp2Init(1, 1, 0x2000, 4); - Require(freshContext > 0); - RequireSslSetters(freshContext, 0); - Require(sceHttp2Term(freshContext) == 0); - + for (const int id : {tmpl, request}) { + Require(Throws([&] { sceHttp2SetMinSslVersion(id, 0); })); + Require(sceHttp2SslEnableOption(id, 0x3d) == 0); + Require(sceHttp2SslDisableOption(id, 0x20) == 0); + Require(Throws([&] { sceHttp2SetMinSslVersion(id, 99); })); + Require(Throws([&] { sceHttp2SslEnableOption(id, 0x80000000); })); + } + for (const int id : {context, box}) { + Require(Throws([&] { sceHttp2SslEnableOption(id, 0x3d); })); + Require(Throws([&] { sceHttp2SslDisableOption(id, 0x20); })); + } + Http2MemoryPoolStats stats{1, 2, 3, 4}; + Require(Throws([&] { sceHttp2GetMemoryPoolStats(context, &stats); })); + Require(stats.pool_size == 1 && stats.current_inuse_size == 3); KernelEqueue eq = 0; - Require(sceKernelCreateEqueue(&eq, "http2") == 0); + Require(sceKernelCreateEqueue(&eq, "http2-unsupported-async") == 0); Require(sceKernelAddUserEventEdge(eq, request) == 0); - int tag = 0; Http2AsyncOption option{}; option.equeue = eq; option.user_event_id = request; - option.user_data = &tag; - Require(sceHttp2SendRequestAsync(request, nullptr, 0, &option, nullptr) == 0); - KernelEvent event{}; - int count = 0; - const KernelUseconds timeout = 1000000; - Require(sceKernelWaitEqueue(eq, &event, 1, &count, &timeout) == 0 && count == 1); - Require(sceKernelGetEventId(&event) == static_cast(request)); - Require(sceKernelGetEventUserData(&event) == &tag); - Http2AsyncResult result{}; - Require(sceHttp2WaitAsync(request, &result, nullptr, nullptr) == 0); - Require(result.req_id == request && result.result == static_cast(0x80436063)); - Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); })); - Require(sceHttp2SendRequestAsync(request, nullptr, 0, nullptr, nullptr) == 0); - std::uint32_t wait_timeout = 2000000; - Require(sceHttp2WaitAsync(request, &result, &wait_timeout, nullptr) == 0); - Require(result.req_id == request && result.result == static_cast(0x80436063)); - Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); })); - const int invalidRequests[] = {0, -1, 0x7fffffff, context, tmpl, box, doomedRequest, doomedTemplateRequest, doomedContextRequest}; - for (const int invalid : invalidRequests) { - Require(sceHttp2SendRequestAsync(invalid, nullptr, 0, nullptr, nullptr) == InvalidId); - Require(Throws([&] { sceHttp2WaitAsync(invalid, &result, nullptr, nullptr); })); - } - const int independent = sceHttp2CreateRequestWithURL(tmpl, "GET", "https://example.com/", 0); - Require(independent > 0 && independent != request); - Require(sceHttp2SendRequestAsync(request, nullptr, 0, nullptr, nullptr) == 0); - Require(sceHttp2SendRequestAsync(independent, nullptr, 0, nullptr, nullptr) == 0); - Require(sceHttp2SendRequestAsync(request, nullptr, 0, nullptr, nullptr) == 0); - Require(Throws([&] { sceHttp2SendRequestAsync(request, nullptr, 0, nullptr, &tag); })); - Require(sceHttp2WaitAsync(independent, &result, nullptr, nullptr) == 0); - Require(result.req_id == independent && result.result == static_cast(0x80436063)); - Require(Throws([&] { sceHttp2WaitAsync(independent, &result, nullptr, nullptr); })); - for (int i = 0; i < 2; ++i) { - Require(sceHttp2WaitAsync(request, &result, nullptr, nullptr) == 0); - Require(result.req_id == request && result.result == static_cast(0x80436063)); - } - Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); })); - Require(sceHttp2SendRequestAsync(independent, nullptr, 0, nullptr, nullptr) == 0); - Require(sceHttp2DeleteRequest(independent) == 0); - Require(sceHttp2SendRequestAsync(independent, nullptr, 0, nullptr, nullptr) == InvalidId); - Require(Throws([&] { sceHttp2WaitAsync(independent, &result, nullptr, nullptr); })); - Require(sceKernelDeleteUserEvent(eq, request) == 0); Require(Throws([&] { sceHttp2SendRequestAsync(request, nullptr, 0, &option, nullptr); })); - Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); })); - Require(sceHttp2DeleteRequest(request) == 0); + Http2AsyncResult result{}; + result.req_id = 123; + std::uint32_t timeout = 500; + Require(Throws([&] { sceHttp2WaitAsync(request, &result, &timeout, nullptr); })); + Require(result.req_id == 123 && timeout == 500); + KernelEvent event{}; + int count = 123; + const KernelUseconds poll = 0; + Require(sceKernelWaitEqueue(eq, &event, 1, &count, &poll) != 0); Require(sceKernelDeleteEqueue(eq) == 0); - Require(sceHttp2DeleteCookieBox(box) == 0 && sceHttp2DeleteCookieBox(other) == 0); + const int sibling = sceHttp2CreateTemplate(context, "other", 2, 0); + const int child = sceHttp2CreateRequestWithURL(sibling, "GET", "https://localhost/", 0); + Require(Throws([&] { sceHttp2SendRequest(child, nullptr, 0); })); + Require(sceHttp2DeleteTemplate(tmpl) == 0); + Require(sceHttp2SslEnableOption(sibling, 0) == 0); + Require(sceHttp2DeleteRequest(request) == InvalidId); + Require(sceHttp2SendRequest(request, nullptr, 0) == InvalidId); Require(sceHttp2Term(context) == 0); - - KernelEqueue templateEqueue = 0; - Require(sceKernelCreateEqueue(&templateEqueue, "http2-template") == 0); - const int cascadeContext = sceHttp2Init(1, 1, 0x3000, 4); - Require(cascadeContext > 0); - const int cascadeTemplate = sceHttp2CreateTemplate(cascadeContext, "agent", 2, 0); - Require(cascadeTemplate > 0); - const int cascadeRequest = sceHttp2CreateRequestWithURL(cascadeTemplate, "GET", "https://example.com/cascade", 0); - Require(cascadeRequest > 0); - Require(sceKernelAddUserEventEdge(templateEqueue, cascadeRequest) == 0); - Http2AsyncOption cascadeOption{}; - cascadeOption.equeue = templateEqueue; - cascadeOption.user_event_id = cascadeRequest; - cascadeOption.user_data = &tag; - Require(sceHttp2SendRequestAsync(cascadeRequest, nullptr, 0, &cascadeOption, nullptr) == 0); - KernelEvent cascadeEvent{}; - int cascadeCount = 0; - Require(sceKernelWaitEqueue(templateEqueue, &cascadeEvent, 1, &cascadeCount, &timeout) == 0 && cascadeCount == 1); - Require(sceHttp2DeleteTemplate(cascadeTemplate) == 0); - Require(Throws([&] { sceHttp2WaitAsync(cascadeRequest, &result, nullptr, nullptr); })); - Require(sceHttp2SendRequestAsync(cascadeRequest, nullptr, 0, &cascadeOption, nullptr) == InvalidId); - Require(Throws([&] { sceHttp2WaitAsync(cascadeRequest, &result, nullptr, nullptr); })); - Require(sceHttp2DeleteRequest(cascadeRequest) == InvalidId); - RequireSslSetters(cascadeRequest, InvalidId); - Require(sceHttp2Term(cascadeContext) == 0); - Require(sceKernelDeleteUserEvent(templateEqueue, cascadeRequest) == 0); - Require(sceKernelDeleteEqueue(templateEqueue) == 0); - - KernelEqueue termEqueue = 0; - Require(sceKernelCreateEqueue(&termEqueue, "http2-term") == 0); - const int terminatedContext = sceHttp2Init(1, 1, 0x4000, 4); - Require(terminatedContext > 0); - const int terminatedTemplate = sceHttp2CreateTemplate(terminatedContext, "agent", 2, 0); - Require(terminatedTemplate > 0); - const int terminatedRequest = sceHttp2CreateRequestWithURL(terminatedTemplate, "GET", "https://example.com/terminated", 0); - Require(terminatedRequest > 0); - Require(sceKernelAddUserEventEdge(termEqueue, terminatedRequest) == 0); - Http2AsyncOption termOption{}; - termOption.equeue = termEqueue; - termOption.user_event_id = terminatedRequest; - termOption.user_data = &tag; - Require(sceHttp2SendRequestAsync(terminatedRequest, nullptr, 0, &termOption, nullptr) == 0); - KernelEvent termEvent{}; - int termCount = 0; - Require(sceKernelWaitEqueue(termEqueue, &termEvent, 1, &termCount, &timeout) == 0 && termCount == 1); - Require(sceHttp2Term(terminatedContext) == 0); - Require(sceHttp2SendRequestAsync(terminatedRequest, nullptr, 0, &termOption, nullptr) == InvalidId); - Require(Throws([&] { sceHttp2WaitAsync(terminatedRequest, &result, nullptr, nullptr); })); - Require(sceHttp2DeleteRequest(terminatedRequest) == InvalidId); - Require(sceHttp2DeleteTemplate(terminatedTemplate) == InvalidId); - RequireSslSetters(terminatedContext, InvalidId); - RequireSslSetters(terminatedRequest, InvalidId); - Require(sceKernelDeleteUserEvent(termEqueue, terminatedRequest) == 0); - Require(sceKernelDeleteEqueue(termEqueue) == 0); + Require(sceHttp2DeleteRequest(child) == InvalidId); + Require(sceHttp2DeleteTemplate(sibling) == InvalidId); + Require(sceHttp2SetMinSslVersion(context, 0) == InvalidId); + Require(sceHttp2DeleteCookieBox(box) == 0); + Require(sceHttp2DeleteCookieBox(box) == InvalidId); + Require(sceHttp2Term(context) == InvalidId); } diff --git a/core/libs/tests/GuestHttp2Transport.cpp b/core/libs/tests/GuestHttp2Transport.cpp new file mode 100644 index 0000000000..b785722d08 --- /dev/null +++ b/core/libs/tests/GuestHttp2Transport.cpp @@ -0,0 +1,556 @@ +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#endif + +#include "SceTypes.hpp" +#include "prx/libSceHttp2/include/Http2.hpp" +#include "prx/libSceNet/include/Net.hpp" +#include "prx/libSceSsl/include/Ssl.hpp" +#include "prx/libSceSsl/SslTypes.hpp" +#include "SslCertificateFixture.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) { std::fprintf(stderr, "HTTP2: %s\n", message); std::abort(); } +} + +template +bool Throws(TCall call) { + try { call(); } catch (const std::exception&) { return true; } + return false; +} + +enum class Mode { Normal, Large, NoLength, Head, NotFound, Redirect, Auth, Reset, Truncate, StallHandshake, StallHeaders, StallBody, NoAlpn, Informational, StallUpload, EarlyFinal, EarlyReset, EarlyEmpty, EarlyLarge, EarlyResetIncomplete }; +const char* Protocols[] = {"h2", nullptr}; + +class Server { +public: + explicit Server(Mode mode) : mode(mode) { + mbedtls_net_init(&listener); + mbedtls_entropy_init(&entropy); + mbedtls_ctr_drbg_init(&random); + mbedtls_x509_crt_init(&certificate); + mbedtls_pk_init(&key); + mbedtls_ssl_config_init(&config); + Require(mbedtls_ctr_drbg_seed(&random, mbedtls_entropy_func, &entropy, nullptr, 0) == 0, "server RNG"); + Require(mbedtls_x509_crt_parse(&certificate, reinterpret_cast(ServerPem), sizeof(ServerPem)) == 0, "server cert"); + Require(mbedtls_pk_parse_key(&key, reinterpret_cast(ServerKey), sizeof(ServerKey), nullptr, 0, + mbedtls_ctr_drbg_random, &random) == 0, "server key"); + Require(mbedtls_ssl_config_defaults(&config, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT) == 0, "server TLS"); + mbedtls_ssl_conf_rng(&config, mbedtls_ctr_drbg_random, &random); + Require(mbedtls_ssl_conf_own_cert(&config, &certificate, &key) == 0, "server identity"); + mbedtls_ssl_conf_authmode(&config, MBEDTLS_SSL_VERIFY_NONE); + mbedtls_ssl_conf_read_timeout(&config, 2000); + if (mode != Mode::NoAlpn) Require(mbedtls_ssl_conf_alpn_protocols(&config, Protocols) == 0, "server ALPN"); + Require(mbedtls_net_bind(&listener, "127.0.0.1", "0", MBEDTLS_NET_PROTO_TCP) == 0, "server bind"); + sockaddr_in address{}; +#ifdef _WIN32 + int size = sizeof(address); +#else + socklen_t size = sizeof(address); +#endif + Require(getsockname(listener.fd, reinterpret_cast(&address), &size) == 0, "server port"); + port = ntohs(address.sin_port); + worker = std::thread([this] { Serve(); }); + } + + ~Server() { + Release(); + if (worker.joinable()) worker.join(); + mbedtls_ssl_config_free(&config); + mbedtls_pk_free(&key); + mbedtls_x509_crt_free(&certificate); + mbedtls_ctr_drbg_free(&random); + mbedtls_entropy_free(&entropy); + mbedtls_net_free(&listener); + } + + std::string Url(const char* host = "127.0.0.1") const { + return std::string("https://") + host + ":" + std::to_string(port) + "/path?query=yes#fragment"; + } + + void WaitStage(int desired) { + std::unique_lock lock(mutex); + Require(changed.wait_for(lock, std::chrono::seconds(2), [&] { return stage >= desired; }), "server stage deadline"); + } + + void Release() { + { std::lock_guard lock(mutex); released = true; } + changed.notify_all(); + } + + void Join() { Release(); if (worker.joinable()) worker.join(); } + const std::multimap& Headers() const { return headers; } + const std::string& Upload() const { return upload; } + bool Negotiated() const { return negotiated; } + bool IsEarly() const { return mode >= Mode::EarlyFinal; } +private: + const Mode mode; + mbedtls_net_context listener; + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context random; + mbedtls_x509_crt certificate; + mbedtls_pk_context key; + mbedtls_ssl_config config; + std::thread worker; + std::mutex mutex; + std::condition_variable changed; + bool released = false; + int stage = 0; + std::uint16_t port; + bool negotiated = false; + std::multimap headers; + std::string upload; + std::string body; + std::size_t offset = 0; + bool responded = false; + bool ended = false; + + void Stage(int value) { + { std::lock_guard lock(mutex); stage = value; } + changed.notify_all(); + } + void Stall() { + std::unique_lock lock(mutex); + Require(changed.wait_for(lock, std::chrono::seconds(2), [&] { return released; }), "server release deadline"); + } + + static int Header(nghttp2_session*, const nghttp2_frame*, const std::uint8_t* name, std::size_t nameSize, + const std::uint8_t* value, std::size_t valueSize, std::uint8_t, void* data) { + auto& server = *static_cast(data); + server.headers.emplace(std::string(reinterpret_cast(name), nameSize), std::string(reinterpret_cast(value), valueSize)); + return 0; + } + static int Data(nghttp2_session*, std::uint8_t, std::int32_t, const std::uint8_t* bytes, std::size_t size, void* data) { + static_cast(data)->upload.append(reinterpret_cast(bytes), size); + return 0; + } + static int Frame(nghttp2_session* session, const nghttp2_frame* frame, void* data) { + auto& server = *static_cast(data); + if (server.IsEarly() && frame->hd.type == NGHTTP2_HEADERS && frame->hd.stream_id > 0) { + server.Respond(session, frame->hd.stream_id); + return 0; + } + if (server.mode == Mode::StallUpload && frame->hd.type == NGHTTP2_HEADERS) { server.Stage(2); server.Stall(); } + if (frame->hd.stream_id > 0 && (frame->hd.type == NGHTTP2_HEADERS || frame->hd.type == NGHTTP2_DATA) && + (frame->hd.flags & NGHTTP2_FLAG_END_STREAM)) server.Respond(session, frame->hd.stream_id); + return 0; + } + static int End(nghttp2_session*, std::int32_t, std::uint32_t, void* data) { + static_cast(data)->ended = true; + return 0; + } + static nghttp2_ssize Body(nghttp2_session*, std::int32_t, std::uint8_t* bytes, std::size_t size, + std::uint32_t* flags, nghttp2_data_source*, void* data) { + auto& server = *static_cast(data); + if (server.mode == Mode::StallBody || server.mode == Mode::EarlyResetIncomplete) return NGHTTP2_ERR_DEFERRED; + size = std::min(size, server.body.size() - server.offset); + if (size) std::memcpy(bytes, server.body.data() + server.offset, size); + server.offset += size; + if (server.offset == server.body.size()) *flags |= NGHTTP2_DATA_FLAG_EOF; + return static_cast(size); + } + + void Respond(nghttp2_session* session, std::int32_t stream) { + Require(!responded, "one response per request"); + responded = true; + if (mode == Mode::StallHeaders) { Stage(2); Stall(); return; } + if (mode == Mode::Reset) { Require(nghttp2_submit_rst_stream(session, 0, stream, NGHTTP2_REFUSED_STREAM) == 0, "server reset"); return; } + body = (mode == Mode::Large || mode == Mode::EarlyLarge) ? std::string(200000, 'x') : "response body"; + std::string status = IsEarly() ? "413" : mode == Mode::NotFound ? "404" : mode == Mode::Redirect ? "302" : mode == Mode::Auth ? "401" : "200"; + if (mode == Mode::EarlyEmpty) body.clear(); + auto length = std::to_string(body.size()); + if (mode == Mode::Head) body.clear(); + if (mode == Mode::Truncate) body = "short"; + if (mode == Mode::Informational) { + nghttp2_nv early{reinterpret_cast(const_cast(":status")), reinterpret_cast(const_cast("103")), 7, 3, 0}; + Require(nghttp2_submit_headers(session, 0, stream, nullptr, &early, 1, nullptr) == 0, "server informational response"); + } + std::vector> fields{{":status", status}, {"x-loopback", "yes"}}; + if (mode != Mode::NoLength) fields.emplace_back("content-length", length); + if (mode == Mode::Redirect) fields.emplace_back("location", "/another"); + std::vector response; + for (auto& [name, value] : fields) response.push_back({reinterpret_cast(name.data()), reinterpret_cast(value.data()), name.size(), value.size(), 0}); + nghttp2_data_provider2 provider{}; + provider.read_callback = Body; + Require(nghttp2_submit_response2(session, stream, response.data(), response.size(), body.empty() ? nullptr : &provider) == 0, "server response"); + } + + void Serve() { + mbedtls_net_context client; + mbedtls_net_init(&client); + mbedtls_ssl_context ssl; + mbedtls_ssl_init(&ssl); + Require(mbedtls_net_accept(&listener, &client, nullptr, 0, nullptr) == 0, "server accept"); + Stage(1); + if (mode == Mode::StallHandshake) Stall(); + else { + Require(mbedtls_ssl_setup(&ssl, &config) == 0, "server TLS setup"); + mbedtls_ssl_set_bio(&ssl, &client, mbedtls_net_send, mbedtls_net_recv, mbedtls_net_recv_timeout); + if (mbedtls_ssl_handshake(&ssl) == 0) { + const char* selected = mbedtls_ssl_get_alpn_protocol(&ssl); + negotiated = selected && std::string(selected) == "h2"; + Exchange(ssl); + } + } + mbedtls_ssl_free(&ssl); + mbedtls_net_free(&client); + } + + void Exchange(mbedtls_ssl_context& ssl) { + nghttp2_session_callbacks* callbacks = nullptr; + Require(nghttp2_session_callbacks_new(&callbacks) == 0, "server callbacks"); + nghttp2_session_callbacks_set_on_header_callback(callbacks, Header); + nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, Frame); + nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, Data); + nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, End); + nghttp2_session* session = nullptr; + nghttp2_option* options = nullptr; + Require(nghttp2_option_new(&options) == 0, "server options"); + if (IsEarly()) nghttp2_option_set_no_auto_window_update(options, 1); + Require(nghttp2_session_server_new2(&session, callbacks, this, options) == 0, "server session"); + nghttp2_option_del(options); + nghttp2_session_callbacks_del(callbacks); + Require(nghttp2_submit_settings(session, 0, nullptr, 0) == 0, "server settings"); + bool resetSent = false; + for (;;) { + const std::uint8_t* output = nullptr; + nghttp2_ssize size = 0; + bool sent = true; + while ((size = nghttp2_session_mem_send2(session, &output)) > 0) { + std::size_t position = 0; + while (position < static_cast(size)) { + const auto chunk = std::min(1024, static_cast(size) - position); + const int count = mbedtls_ssl_write(&ssl, output + position, chunk); + if (count <= 0) { sent = false; break; } + position += static_cast(count); + } + if (!sent) break; + } + if (IsEarly() && responded && sent) { + Stage(2); + if (!resetSent && (mode == Mode::EarlyReset || mode == Mode::EarlyResetIncomplete)) { + Require(nghttp2_submit_rst_stream(session, 0, 1, NGHTTP2_NO_ERROR) == 0, "early response reset"); + resetSent = true; + continue; + } + if (mode != Mode::EarlyLarge) { Stall(); break; } + } + if (mode == Mode::StallBody && responded) { Stage(2); Stall(); break; } + if (ended && sent) { + mbedtls_ssl_close_notify(&ssl); + std::array drain; + while (mbedtls_ssl_read(&ssl, drain.data(), drain.size()) > 0) {} + break; + } + if (!sent || (mode == Mode::StallHeaders && responded)) break; + std::array input; + const int count = mbedtls_ssl_read(&ssl, input.data(), input.size()); + if (count <= 0 || nghttp2_session_mem_recv2(session, input.data(), static_cast(count)) < 0) { + std::fprintf(stderr, "server stopped count %d upload %zu response %zu/%zu ended %d\n", count, upload.size(), offset, body.size(), ended); + break; + } + } + nghttp2_session_del(session); + } +}; + +struct Client { + int context, tmpl, request = 0; + explicit Client(int ssl, int maximum = 4, int autoProxy = 0) { + context = sceHttp2Init(1, ssl, 1024 * 1024, maximum); + tmpl = sceHttp2CreateTemplate(context, "loopback-agent", 3, autoProxy); + Require(context > 0 && tmpl > 0, "client handles"); + Require(sceHttp2SslDisableOption(tmpl, 0x4) == 0, "numeric loopback name policy"); + } + void Create(const std::string& url, const char* method = "GET", std::size_t size = 0) { + request = sceHttp2CreateRequestWithURL(tmpl, method, url.c_str(), size); + Require(request > 0, "request handle"); + } + ~Client() { if (context > 0) Require(sceHttp2Term(context) == 0, "client term"); } +}; + +void Success(int ssl, Mode mode, bool disableCa = false) { + std::fprintf(stderr, "HTTP2 success scenario %d\n", static_cast(mode)); + Server server(mode); + Client client(ssl); + if (disableCa) Require(sceHttp2SslDisableOption(client.tmpl, 0x20) == 0, "disable known CA check"); + if (mode == Mode::Normal && !disableCa) Require(sceHttp2SslEnableOption(client.tmpl, 0x4) == 0, "verify localhost certificate name"); + Require(sceHttp2AddRequestHeader(client.tmpl, "X-Inherited", "original", 0) == 0, "template header"); + const std::string upload = mode == Mode::Large ? std::string(150000, 'u') : std::string{}; + client.Create(server.Url(mode == Mode::Normal && !disableCa ? "localhost" : "127.0.0.1"), mode == Mode::Head ? "HEAD" : upload.empty() ? "GET" : "POST", upload.size()); + Require(sceHttp2AddRequestHeader(client.tmpl, "X-Inherited", "changed", 0) == 0, "template mutation"); + Require(sceHttp2AddRequestHeader(client.request, "X-Duplicate", "first", 0) == 0, "overwrite header"); + Require(sceHttp2AddRequestHeader(client.request, "x-duplicate", "last", 0) == 0, "case insensitive overwrite"); + Require(sceHttp2AddRequestHeader(client.request, "x-duplicate", "appended", 1) == 0, "append header"); + Require(sceHttp2SetAutoRedirect(client.request, 0) == 0 && sceHttp2SetAuthEnabled(client.request, 0) == 0, "manual policies"); + if (mode == Mode::NoLength) Require(sceHttp2SetRequestNoContentLength(client.request) == 0, "omit request length"); + Require(sceHttp2SendRequest(client.request, upload.data(), upload.size()) == 0, "send real request"); + int status = -1; + Require(sceHttp2GetStatusCode(client.request, &status) == 0, "status getter"); + const int expected = mode == Mode::NotFound ? 404 : mode == Mode::Redirect ? 302 : mode == Mode::Auth ? 401 : 200; + Require(status == expected, "actual server status"); + int kind = -1; + std::uint64_t length = 999; + Require(sceHttp2GetResponseContentLength(client.request, &kind, &length) == 0, "length getter"); + Require(kind == (mode == Mode::NoLength ? 1 : 0), "length kind"); + if (kind == 0) Require(length == (mode == Mode::Large ? 200000 : 13), "actual content length"); + std::string body; + std::array buffer; + for (;;) { + const int count = sceHttp2ReadData(client.request, buffer.data(), buffer.size()); + Require(count >= 0 && count <= static_cast(buffer.size()), "stream read count"); + if (count == 0) break; + body.append(buffer.data(), static_cast(count)); + } + Require(body == (mode == Mode::Head ? "" : mode == Mode::Large ? std::string(200000, 'x') : "response body"), "stream body"); + Require(sceHttp2ReadData(client.request, buffer.data(), buffer.size()) == 0, "repeat EOF"); + server.Join(); + Require(server.Negotiated(), "actual h2 ALPN"); + Require(server.Upload() == upload, "server received upload"); + const auto& headers = server.Headers(); + Require(headers.find(":path")->second == "/path?query=yes", "query retained and fragment omitted"); + Require(headers.find("x-inherited")->second == "original", "settings snapshot"); + Require(headers.find("user-agent")->second == "loopback-agent", "user agent"); + Require(headers.count("content-length") == (mode == Mode::NoLength ? 0u : 1u), "request length policy"); + Require(headers.count("x-duplicate") == 2, "duplicate count"); + Require(headers.find("x-duplicate")->second == "last", "duplicate overwrite"); + char* rawHeaders = reinterpret_cast(std::uintptr_t{1}); + std::size_t rawSize = 987; + Require(Throws([&] { sceHttp2GetAllResponseHeaders(client.request, &rawHeaders, &rawSize); }) && + rawHeaders == reinterpret_cast(std::uintptr_t{1}) && rawSize == 987, "unknown header serialization leaves outputs untouched"); + Require(Throws([&] { sceHttp2SendRequest(client.request, nullptr, 0); }), "unsupported reuse throws"); +} + +void Failure(int ssl, Mode mode, bool verifyName = false) { + std::fprintf(stderr, "HTTP2 failure scenario %d\n", static_cast(mode)); + Server server(mode); + Client client(ssl); + if (verifyName) Require(sceHttp2SslEnableOption(client.tmpl, 0x4) == 0, "enable numeric host name rejection"); + client.Create(server.Url()); + Require(sceHttp2SetConnectTimeOut(client.request, 1000000) == 0 && sceHttp2SetRecvTimeOut(client.request, 1000000) == 0, "failure deadlines"); + Require(Throws([&] { + sceHttp2SendRequest(client.request, nullptr, 0); + std::array buffer; + while (sceHttp2ReadData(client.request, buffer.data(), buffer.size()) > 0) {} + }), "failure never reports complete success"); + int status = 1234; + Require(Throws([&] { sceHttp2GetStatusCode(client.request, &status); }) && status == 1234, "failed outputs unchanged"); +} + +void Cancellation(int ssl, Mode mode, int teardown) { + std::fprintf(stderr, "HTTP2 cancellation scenario %d teardown %d\n", static_cast(mode), teardown); + Server server(mode); + Client client(ssl); + const std::string upload = mode == Mode::StallUpload ? std::string(150000, 'u') : std::string{}; + client.Create(server.Url(), upload.empty() ? "GET" : "POST", upload.size()); + SceSsl::MemoryPoolStats before{}; + Require(sceSslGetMemoryPoolStats(ssl, &before) == 0, "TLS pool baseline"); + auto pending = std::async(std::launch::async, [&] { + try { + const int sent = sceHttp2SendRequest(client.request, upload.data(), upload.size()); + if (sent == static_cast(0x817b1100)) return teardown != 0; + Require(sent == 0, "pending send result"); + char byte = 'z'; + const int read = sceHttp2ReadData(client.request, &byte, 1); + if (read == static_cast(0x817b1100)) { + Require(byte == 'z', "deleted-handle read leaves buffer untouched"); + return teardown != 0; + } + return false; + } catch (const std::exception&) { return true; } + }); + server.WaitStage(mode == Mode::StallHandshake ? 1 : 2); + Require(pending.wait_for(std::chrono::milliseconds(0)) == std::future_status::timeout, "operation is pending"); + if (teardown == 0) Require(sceHttp2AbortRequest(client.request) == 0, "cancel request"); + if (teardown == 1) Require(sceHttp2DeleteRequest(client.request) == 0, "delete pending request"); + if (teardown == 2) Require(sceHttp2DeleteTemplate(client.tmpl) == 0, "delete pending template"); + if (teardown == 3) { Require(sceHttp2Term(client.context) == 0, "term pending context"); client.context = 0; } + if (teardown) { + SceSsl::MemoryPoolStats after{}; + Require(sceSslGetMemoryPoolStats(ssl, &after) == 0 && after.currentInuseSize == before.currentInuseSize, + "teardown releases TLS allocations before returning"); + } + Require(pending.wait_for(std::chrono::seconds(1)) == std::future_status::ready && pending.get(), "pending call observes cancellation"); + if (teardown) Require(sceHttp2DeleteRequest(client.request) == static_cast(0x817b1100), "deleted request stays invalid"); +} + +void EarlyResponse(int ssl, Mode mode) { + std::fprintf(stderr, "HTTP2 early response scenario %d\n", static_cast(mode)); + Server server(mode); + Client client(ssl, 1, 1); + const std::string upload(150000, 'u'); + client.Create(server.Url(), "POST", upload.size()); + auto pending = std::async(std::launch::async, [&] { + Require(sceHttp2SendRequest(client.request, upload.data(), upload.size()) == 0, "early final send succeeds"); + int status = -1; + Require(sceHttp2GetStatusCode(client.request, &status) == 0 && status == 413, "early final status"); + int kind = -1; + std::uint64_t length = 999; + const std::string expected = mode == Mode::EarlyEmpty ? "" : mode == Mode::EarlyLarge ? std::string(200000, 'x') : "response body"; + Require(sceHttp2GetResponseContentLength(client.request, &kind, &length) == 0 && kind == 0 && length == expected.size(), "early final length"); + std::string body; + std::array buffer; + for (;;) { + const int count = sceHttp2ReadData(client.request, buffer.data(), buffer.size()); + Require(count >= 0 && count <= static_cast(buffer.size()), "early final read count"); + if (!count) break; + body.append(buffer.data(), static_cast(count)); + } + Require(body == expected, "early final body"); + Require(sceHttp2ReadData(client.request, buffer.data(), buffer.size()) == 0, "early final repeat EOF"); + }); + server.WaitStage(2); + Require(pending.wait_for(std::chrono::seconds(1)) == std::future_status::ready, "early response completes before upload release"); + pending.get(); + server.Join(); + Require(server.Upload().size() <= 65535 && server.Upload().size() < upload.size(), "early final stops upload within initial window"); + Require(server.Negotiated(), "early final h2 ALPN"); +} + +#ifdef _WIN32 +void FirstNetworkRequest(int ssl) { + WSADATA data{}; + Require(WSAStartup(MAKEWORD(2, 2), &data) == 0, "reserve refused endpoint startup"); + const SOCKET listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + Require(listener != INVALID_SOCKET, "reserve refused endpoint socket"); + sockaddr_in address{}; + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + Require(bind(listener, reinterpret_cast(&address), sizeof(address)) == 0, "reserve refused endpoint bind"); + int size = sizeof(address); + Require(getsockname(listener, reinterpret_cast(&address), &size) == 0, "reserve refused endpoint port"); + Require(closesocket(listener) == 0 && WSACleanup() == 0, "return to uninitialized Winsock"); + addrinfo* addresses = nullptr; + Require(getaddrinfo("localhost", nullptr, nullptr, &addresses) == WSANOTINITIALISED && !addresses, "DNS starts without Winsock"); + Client client(ssl, 1, 1); + client.Create("https://localhost:" + std::to_string(ntohs(address.sin_port)) + "/"); + Require(sceHttp2SendRequest(client.request, nullptr, 0) == static_cast(0x8041013d), "first network request resolves and reaches refused endpoint"); +} +#endif + +void Refused(int ssl) { + mbedtls_net_context listener; + mbedtls_net_init(&listener); + Require(mbedtls_net_bind(&listener, "127.0.0.1", "0", MBEDTLS_NET_PROTO_TCP) == 0, "refused bind"); + sockaddr_in address{}; +#ifdef _WIN32 + int size = sizeof(address); +#else + socklen_t size = sizeof(address); +#endif + Require(getsockname(listener.fd, reinterpret_cast(&address), &size) == 0, "refused port"); + const auto url = "https://127.0.0.1:" + std::to_string(ntohs(address.sin_port)) + "/"; + mbedtls_net_free(&listener); + Client client(ssl); + client.Create(url); + Require(sceHttp2SendRequest(client.request, nullptr, 0) == static_cast(0x8041013d), "raw connection refused error"); + int status = 123; + Require(sceHttp2GetStatusCode(client.request, &status) == static_cast(0x8041013d) && status == 123, "failed status untouched"); +} + +void Capacity(int ssl) { + Server server(Mode::StallBody); + Client client(ssl, 1); + client.Create(server.Url()); + Require(sceHttp2SendRequest(client.request, nullptr, 0) == 0, "first capacity slot"); + const int second = sceHttp2CreateRequestWithURL(client.tmpl, "GET", server.Url().c_str(), 0); + Require(Throws([&] { sceHttp2SendRequest(second, nullptr, 0); }), "capacity wait throws before another connection"); + Require(sceHttp2DeleteRequest(client.request) == 0, "release capacity slot"); + char unchanged = 'z'; + Require(sceHttp2ReadData(client.request, &unchanged, 1) == static_cast(0x817b1100) && unchanged == 'z', + "read after deletion leaves buffer untouched"); + Server independent(Mode::Normal); + const int third = sceHttp2CreateRequestWithURL(client.tmpl, "GET", independent.Url().c_str(), 0); + Require(sceHttp2SendRequest(third, nullptr, 0) == 0, "slot available after deletion"); + char buffer[32]; + while (sceHttp2ReadData(third, buffer, sizeof(buffer)) > 0) {} +} + +void Deadline(int ssl, Mode mode, bool overall) { + std::fprintf(stderr, "HTTP2 deadline scenario %d overall %d\n", static_cast(mode), overall); + Server server(mode); + Client client(ssl); + const std::string upload = mode == Mode::StallUpload ? std::string(150000, 'u') : std::string{}; + client.Create(server.Url(), upload.empty() ? "GET" : "POST", upload.size()); + const auto begin = std::chrono::steady_clock::now(); + Require((overall ? sceHttp2SetTimeOut(client.request, 80000) : mode == Mode::StallHandshake ? + sceHttp2SetConnectTimeOut(client.request, 80000) : mode == Mode::StallUpload ? sceHttp2SetSendTimeOut(client.request, 80000) : + sceHttp2SetRecvTimeOut(client.request, 80000)) == 0, "set deadline"); + Require(Throws([&] { + sceHttp2SendRequest(client.request, upload.data(), upload.size()); + char byte; + sceHttp2ReadData(client.request, &byte, 1); + }), "deadline terminates blocked operation"); + const auto elapsed = std::chrono::steady_clock::now() - begin; + Require(elapsed >= std::chrono::milliseconds(60) && elapsed < std::chrono::seconds(1), "bounded deadline"); +} + +} + +int main(int argc, char** argv) { +#ifndef _WIN32 + std::signal(SIGPIPE, SIG_IGN); +#endif + Require(sceNetInit_nid_postfix() == 0, "net init"); + const int ssl = sceSslInit_nid_postfix(8 * 1024 * 1024); + Require(ssl > 0, "SSL context"); + SceSsl::Data root{reinterpret_cast(const_cast(RootPem)), sizeof(RootPem)}; + SceSsl::Data* authorities[] = {&root}; + Require(sceSslLoadCert(ssl, 1, authorities, nullptr, nullptr) == 0, "trust loopback CA"); + if (argc == 2 && std::string_view(argv[1]) == "--early") { + for (const auto mode : {Mode::EarlyFinal, Mode::EarlyReset, Mode::EarlyEmpty, Mode::EarlyLarge}) EarlyResponse(ssl, mode); + Failure(ssl, Mode::EarlyResetIncomplete); +#ifdef _WIN32 + } else if (argc == 2 && std::string_view(argv[1]) == "--dns-first") { + FirstNetworkRequest(ssl); +#endif + } else { + Require(argc == 1, "test arguments"); + for (const auto mode : {Mode::Normal, Mode::Large, Mode::NoLength, Mode::Head, Mode::NotFound, Mode::Redirect, Mode::Auth, Mode::Informational}) Success(ssl, mode); + for (const auto mode : {Mode::NoAlpn, Mode::Reset, Mode::Truncate, Mode::Redirect, Mode::Auth}) Failure(ssl, mode); + Failure(ssl, Mode::Normal, true); + for (const auto mode : {Mode::StallHandshake, Mode::StallHeaders, Mode::StallBody, Mode::StallUpload}) { + Deadline(ssl, mode, false); + Deadline(ssl, mode, true); + for (int teardown = 0; teardown < 4; ++teardown) Cancellation(ssl, mode, teardown); + } + Refused(ssl); + Capacity(ssl); + const int untrusted = sceSslInit_nid_postfix(8 * 1024 * 1024); + Require(untrusted > 0, "untrusted SSL context"); + Failure(untrusted, Mode::Normal); + Success(untrusted, Mode::Normal, true); + Require(sceSslTerm_nid_postfix(untrusted) == 0, "untrusted SSL term"); + } + Require(sceSslTerm_nid_postfix(ssl) == 0, "SSL term"); + std::puts("HTTP2 loopback transport passed"); +} diff --git a/core/libs/tests/GuestHttpCache.cpp b/core/libs/tests/GuestHttpCache.cpp new file mode 100644 index 0000000000..c89f17da12 --- /dev/null +++ b/core/libs/tests/GuestHttpCache.cpp @@ -0,0 +1,444 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceHttpCache/libSceHttpCache.hpp" +#include "prx/libSceRtc/libSceRtc.hpp" +#include "prx/libSceUserService/libSceUserService.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using Header = HttpCacheHeader; +using Request = HttpCacheRequest; +using ResponseHeader = HttpCacheResponseHeader; +using Response = HttpCacheResponse; +using QuotaEntry = HttpCacheQuotaEntry; +using Quota = HttpCacheQuota; + +} + +namespace { + +constexpr int NotFound = static_cast(0x817a0011); +constexpr int InvalidValue = static_cast(0x817a0012); +constexpr int InvalidUser = static_cast(0x817a0013); +constexpr int TooManyCaches = static_cast(0x817a0002); +constexpr int UserServiceAlreadyInitialized = static_cast(0x80960003); +constexpr std::int32_t InitialUser = 0x10000000; + +void Require(bool value) { if (!value) std::abort(); } + +void TestWithoutCache() { + Require(sceHttpCacheTerm(1) == NotFound); + Require(sceHttpCacheCreateRequest(1, -1) == NotFound); + Require(sceHttpCacheClearAll(1, -1) == NotFound); + Require(sceHttpCacheCompleteRequest(0x01000001) == NotFound); + Require(sceHttpCacheDeleteRequest(0x01000001) == NotFound); + Require(sceHttpCacheSetCacheSharing(1) == 0); + Require(sceHttpCacheSystemSendStatistics() == 0); + Require(sceHttpCacheSystemTerm() == 0); + Require(sceHttpCacheDebugSetUrl() == 0); + Require(sceHttpCacheRegisterCacheInfoUrl(nullptr) == InvalidValue); +} + +void TestRequests() { + const int cache = sceHttpCacheInit(0); + const int second = sceHttpCacheInit(0x10000); + Require(cache == 1 && second == 2); + Require(sceUserServiceInitialize(nullptr) == UserServiceAlreadyInitialized); + Require(sceHttpCacheCreateRequest(9, -1) == NotFound); + Require(sceHttpCacheCreateRequest(cache, 5) == InvalidUser); + Require(sceHttpCacheCreateRequest(cache, 0x10000001) == InvalidUser); + const int request = sceHttpCacheCreateRequest(cache, -1); + Require(request == 0x01000001); + Require(sceHttpCacheCreateRequest(cache, 255) == 0x01000002); + Require(sceHttpCacheCreateRequest(cache, InitialUser) == 0x01000003); + Require(sceHttpCacheCreateRequest(cache, 0xfe) == 0x01000004); + Require(sceHttpCacheCreateRequest(second, -1) == 0x02000001); + Require(sceHttpCacheCompleteRequest(request) == 0); + Require(sceHttpCacheCompleteRequest(0x01000099) == NotFound); + Require(sceHttpCacheCompleteRequest(0x09000001) == NotFound); + std::array buffer{1, 2, 3, 4}; + Require(sceHttpCacheReadData(request, 4, nullptr) == InvalidValue); + Require(sceHttpCacheReadData(request, 0, nullptr) == 0); + Require(sceHttpCacheReadData(request, buffer.size(), buffer.data()) == 0); + Require(buffer[0] == 1 && buffer[3] == 4); + Require(sceHttpCacheReadData(0x01000099, buffer.size(), buffer.data()) == NotFound); + Require(sceHttpCacheSetData(request, nullptr, 4) == InvalidValue); + Require(sceHttpCacheSetData(0x01000099, buffer.data(), buffer.size()) == NotFound); + Require(sceHttpCacheSetData(request, buffer.data(), buffer.size()) == 0); + Request query{}; + Response response{}; + Request next{}; + ResponseHeader header{}; + std::int32_t result = 0; + Require(sceHttpCacheRetrieve(request, nullptr, &response, &next, &result) == InvalidValue); + Require(sceHttpCacheRetrieve(request, &query, &response, &next, nullptr) == InvalidValue); + Require(sceHttpCacheRetrieve(0x01000099, &query, &response, &next, &result) == NotFound); + const Header nullName[]{{nullptr, "a"}}; + query.url = "http://a/"; + query.headerCount = 1; + Require(sceHttpCacheRetrieve(request, &query, &response, &next, &result) == InvalidValue); + query.headers = nullName; + Require(sceHttpCacheRetrieve(request, &query, &response, &next, &result) == InvalidValue); + query.headerCount = 0; + result = 7; + Require(sceHttpCacheRetrieve(request, &query, &response, &next, &result) == 0 && result == 0); + Require(sceHttpCacheRevalidate(request, nullptr, &response, &result) == InvalidValue); + Require(sceHttpCacheRevalidate(request, &header, &response, nullptr) == InvalidValue); + Require(sceHttpCacheRevalidate(0x01000099, &header, &response, &result) == NotFound); + result = 7; + Require(sceHttpCacheRevalidate(request, &header, &response, &result) == 0 && result == 0); + Require(sceHttpCacheSetResponseHeader(request, nullptr) == InvalidValue); + Require(sceHttpCacheSetResponseHeader(0x01000099, &header) == NotFound); + Require(sceHttpCacheSetResponseHeader(request, &header) == 0); + header.headerCount = 1; + Require(sceHttpCacheSetResponseHeader(request, &header) == InvalidValue); + Require(sceHttpCacheRevalidate(request, &header, &response, &result) == InvalidValue); + header.headerCount = 0; + Require(sceHttpCacheDeleteRequest(0x01000099) == 0); + Require(sceHttpCacheDeleteRequest(0x09000001) == NotFound); + Require(sceHttpCacheDeleteRequest(request) == 0); + Require(sceHttpCacheCompleteRequest(request) == NotFound); + Require(sceHttpCacheReadData(request, 0, nullptr) == NotFound); + Require(sceHttpCacheCreateRequest(cache, -1) == 0x01000005); + Require(sceHttpCacheClearAll(cache, -1) == 0); + Require(sceHttpCacheTerm(second) == 0); + Require(sceHttpCacheCompleteRequest(0x02000001) == NotFound); + Require(sceHttpCacheTerm(cache) == 0); + Require(sceHttpCacheTerm(cache) == NotFound); + Require(sceUserServiceInitialize(nullptr) == 0); + Require(sceUserServiceTerminate() == 0); +} + +void TestCacheArguments() { + const int cache = sceHttpCacheInit(0); + Require(cache == 3); + Require(sceHttpCacheCreateRequest(cache, -1) == 0x03000001); + Require(sceHttpCacheSetQuota(9, nullptr) == NotFound); + Require(sceHttpCacheSetQuota(cache, nullptr) == InvalidValue); + Quota quota{1, nullptr}; + Require(sceHttpCacheSetQuota(cache, "a) == InvalidValue); + QuotaEntry entries[]{{"", 1}}; + quota.entries = entries; + Require(sceHttpCacheSetQuota(cache, "a) == InvalidValue); + entries[0].name = "a"; + const std::int32_t users[]{InitialUser}; + Require(sceHttpCacheClearAllUserTags(9, users, 1) == NotFound); + Require(sceHttpCacheClearAllUserTags(cache, nullptr, 1) == InvalidValue); + Require(sceHttpCacheClearAllUserTags(cache, users, 21) == InvalidValue); + Require(sceHttpCacheTerm(cache) == 0); +} + +void TestCacheLimit() { + std::vector caches; + for (int i = 0; i < 127; ++i) { + const int cache = sceHttpCacheInit(0); + Require(cache >= 1 && cache <= 127); + caches.push_back(cache); + } + Require(caches.front() == 4 && caches[123] == 127 && caches[124] == 1 && caches.back() == 3); + Require(sceHttpCacheInit(0) == TooManyCaches); + Require(sceHttpCacheTerm(caches[10]) == 0); + Require(sceHttpCacheInit(0) == caches[10]); + for (const int cache : caches) Require(sceHttpCacheTerm(cache) == 0); +} + +constexpr std::uint64_t TicksPerSecond = 1000000; + +void Advance(std::uint64_t seconds) { + RtcTick tick{}; + Require(sceRtcGetCurrentTick(&tick) == 0); + tick.tick += seconds * TicksPerSecond; + Require(sceRtcSetCurrentTick(&tick) == 0); +} + +std::string_view Value(const Response& response, std::string_view name) { + for (std::size_t i = 0; i < response.headerCount; ++i) { + if (name == response.headers[i].name) return response.headers[i].value; + } + return {}; +} + +std::string_view Value(const Request& request, std::string_view name) { + for (std::size_t i = 0; i < request.headerCount; ++i) { + if (name == request.headers[i].name) return request.headers[i].value; + } + return {}; +} + +std::string ReadAll(int request) { + std::string body; + char buffer[4]; + for (;;) { + const int read = sceHttpCacheReadData(request, sizeof(buffer), buffer); + Require(read >= 0 && read <= static_cast(sizeof(buffer))); + if (read == 0) return body; + body.append(buffer, static_cast(read)); + } +} + +struct Exchange { + int cache; + std::int32_t user = InitialUser; + std::uint32_t method = 0; + std::vector
requestHeaders; + Request next{}; + Response response{}; + std::int32_t result = -1; + int request = 0; + + int Retrieve(const char* url) { + request = sceHttpCacheCreateRequest(cache, user); + Require(request > 0); + Request query{method, 0, url, requestHeaders.size(), requestHeaders.data()}; + next = Request{}; + response = Response{}; + result = -1; + return sceHttpCacheRetrieve(request, &query, &response, &next, &result); + } + + void Store(std::uint32_t status, std::vector
headers, std::string_view body) { + ResponseHeader header{status, 0, headers.size(), headers.data()}; + Require(sceHttpCacheSetResponseHeader(request, &header) == 0); + Require(sceHttpCacheSetData(request, body.data(), body.size()) == 0); + Require(sceHttpCacheReadData(request, 0, nullptr) == 0); + Require(sceHttpCacheCompleteRequest(request) == 0); + } + + int Revalidate(std::uint32_t status, std::vector
headers) { + ResponseHeader header{status, 0, headers.size(), headers.data()}; + response = Response{}; + result = -1; + return sceHttpCacheRevalidate(request, &header, &response, &result); + } + + void Done() { + Require(sceHttpCacheDeleteRequest(request) == 0); + } +}; + +void Cached(Exchange& exchange, const char* url, std::vector
headers, std::string_view body) { + Require(exchange.Retrieve(url) == 0 && exchange.result == 0); + exchange.Store(200, std::move(headers), body); + exchange.Done(); +} + +void RequireMiss(Exchange& exchange, const char* url) { + Require(exchange.Retrieve(url) == 0 && exchange.result == 0); + exchange.Done(); +} + +void TestStoreAndHit() { + Exchange exchange{sceHttpCacheInit(0)}; + exchange.requestHeaders = {{"User-Agent", "agent"}}; + Require(exchange.Retrieve("http://store/a") == 0 && exchange.result == 0); + exchange.Store(200, {{"Cache-Control", "max-age=3600"}, {"ETag", "\"v1\""}}, "hello world"); + Require(sceHttpCacheSetData(exchange.request, "!", 1) == 0); + exchange.Done(); + Require(exchange.Retrieve("http://store/a") == 0 && exchange.result == 1); + Require(exchange.response.statusCode == 200 && exchange.response.headerCount == 2 && exchange.response.contentLength == 11); + Require(Value(exchange.response, "Cache-Control") == "max-age=3600" && Value(exchange.response, "ETag") == "\"v1\""); + Require(ReadAll(exchange.request) == "hello world"); + Request again{0, 0, "http://store/a", 0, nullptr}; + std::int32_t result = -1; + Require(sceHttpCacheRetrieve(exchange.request, &again, nullptr, &exchange.next, &result) == InvalidValue && result == 1); + exchange.Done(); + exchange.requestHeaders = {{"User-Agent", "other"}}; + RequireMiss(exchange, "http://store/a"); + exchange.requestHeaders = {{"user-agent", "agent"}, {"If-None-Match", "\"v1\""}}; + RequireMiss(exchange, "http://store/a"); + exchange.requestHeaders = {{"User-Agent", "agent"}}; + exchange.method = 1; + RequireMiss(exchange, "http://store/a"); + exchange.method = 0; + exchange.user = -1; + RequireMiss(exchange, "http://store/a"); + Cached(exchange, "http://store/system", {{"Cache-Control", "max-age=3600"}}, "x"); + RequireMiss(exchange, "http://store/system"); + exchange.user = InitialUser; + Require(exchange.Retrieve("http://store/a") == 0 && exchange.result == 1); + exchange.Done(); + Require(exchange.Retrieve("http://store/a") == 0 && exchange.result == 1); + exchange.Store(200, {{"Content-Type", "text/plain"}}, "uncacheable"); + exchange.Done(); + RequireMiss(exchange, "http://store/a"); + Cached(exchange, "http://store/b", {{"Cache-Control", "no-store, max-age=60"}}, "x"); + RequireMiss(exchange, "http://store/b"); + Require(exchange.Retrieve("http://store/b") == 0 && exchange.result == 0); + exchange.Store(404, {{"Cache-Control", "max-age=60"}}, "x"); + exchange.Done(); + RequireMiss(exchange, "http://store/b"); + Require(exchange.Retrieve("http://store/c") == 0 && exchange.result == 0); + ResponseHeader pending{200, 0, 1, nullptr}; + Header maxAge[]{{"Cache-Control", "max-age=60"}}; + pending.headers = maxAge; + Require(sceHttpCacheSetResponseHeader(exchange.request, &pending) == 0); + Require(sceHttpCacheSetData(exchange.request, "lost", 4) == 0); + exchange.Done(); + RequireMiss(exchange, "http://store/c"); + Cached(exchange, "http://store/d", {{"Expires", "x"}}, "expires"); + Require(exchange.Retrieve("http://store/d") == 0 && exchange.result == 0); + exchange.Done(); + Cached(exchange, "http://store/e", {{"Cache-Control", "max-age=60"}, {"Vary", "Accept-Language"}}, "en"); + exchange.requestHeaders = {{"User-Agent", "agent"}, {"Accept-Language", "fr"}}; + RequireMiss(exchange, "http://store/e"); + exchange.requestHeaders = {{"User-Agent", "agent"}}; + Require(exchange.Retrieve("http://store/e") == 0 && exchange.result == 1); + exchange.Done(); + Require(sceHttpCacheClearAll(exchange.cache, -1) == 0); + Require(sceHttpCacheClearAll(exchange.cache, 5) == 0); + Require(exchange.Retrieve("http://store/e") == 0 && exchange.result == 1); + exchange.Done(); + Require(sceHttpCacheTerm(exchange.cache) == 0); + exchange.cache = sceHttpCacheInit(0); + Require(exchange.Retrieve("http://store/e") == 0 && exchange.result == 1); + exchange.Done(); + Require(sceHttpCacheClearAll(exchange.cache, InitialUser) == 0); + RequireMiss(exchange, "http://store/e"); + Require(sceHttpCacheTerm(exchange.cache) == 0); +} + +void TestLifetimeOrder() { + Exchange exchange{sceHttpCacheInit(0)}; + Cached(exchange, "http://order/expires-later", {{"Date", "Sat, 01 Jan 2000 00:00:00 GMT"}, {"Expires", "Sat, 01 Jan 2000 01:00:00 GMT"}}, "x"); + RequireMiss(exchange, "http://order/expires-later"); + Cached(exchange, "http://order/expires-earlier", {{"Date", "Sat, 01 Jan 2000 01:00:00 GMT"}, {"Expires", "Sat, 01 Jan 2000 00:00:00 GMT"}}, "expired"); + Require(exchange.Retrieve("http://order/expires-earlier") == 0 && exchange.result == 1); + Require(ReadAll(exchange.request) == "expired" && Value(exchange.response, "Warning").empty()); + exchange.Done(); + Cached(exchange, "http://order/modified-earlier", {{"Date", "Mon, 10 Jan 2000 00:00:00 GMT"}, {"Last-Modified", "Sat, 01 Jan 2000 00:00:00 GMT"}}, "x"); + Require(exchange.Retrieve("http://order/modified-earlier") == 0 && exchange.result == 2); + Require(Value(exchange.next, "If-Modified-Since") == "Sat, 01 Jan 2000 00:00:00 GMT"); + exchange.Done(); + Cached(exchange, "http://order/modified-later", {{"Date", "Sat, 01 Jan 2000 00:00:00 GMT"}, {"Last-Modified", "Sun, 02 Jan 2000 00:00:00 GMT"}}, "modified"); + Advance(400 * 24 * 3600); + Require(exchange.Retrieve("http://order/modified-later") == 0 && exchange.result == 1); + Require(ReadAll(exchange.request) == "modified" && !Value(exchange.response, "Warning").empty()); + exchange.Done(); + Require(sceHttpCacheTerm(exchange.cache) == 0); +} + +void TestRevalidation() { + Exchange exchange{sceHttpCacheInit(0)}; + Cached(exchange, "http://stale/a", {{"Cache-Control", "max-age=60"}, {"ETag", "e1"}, {"Last-Modified", "lm"}}, "old body"); + Advance(120); + exchange.requestHeaders = {{"Accept", "*/*"}}; + Require(exchange.Retrieve("http://stale/a") == 0 && exchange.result == 2); + Require(std::string_view(exchange.next.url) == "http://stale/a" && exchange.next.method == 0 && exchange.next.headerCount == 3); + Require(Value(exchange.next, "Accept") == "*/*" && Value(exchange.next, "If-Modified-Since") == "lm" && Value(exchange.next, "If-None-Match") == "e1"); + Require(exchange.response.headers == nullptr); + Require(sceHttpCacheReadData(exchange.request, 0, nullptr) == 0); + Request query{0, 0, "http://stale/a", 0, nullptr}; + std::int32_t result = -1; + Require(sceHttpCacheRetrieve(exchange.request, &query, nullptr, nullptr, &result) == InvalidValue && result == 2); + Require(exchange.Revalidate(304, {{"Cache-Control", "max-age=600"}, {"ETag", "e2"}, {"X-New", "1"}}) == 0 && exchange.result == 1); + Require(exchange.response.statusCode == 200 && exchange.response.contentLength == 8); + Require(Value(exchange.response, "Cache-Control") == "max-age=600" && Value(exchange.response, "ETag") == "e1" && Value(exchange.response, "X-New").empty()); + Require(ReadAll(exchange.request) == "old body"); + exchange.Done(); + Advance(120); + Require(exchange.Retrieve("http://stale/a") == 0 && exchange.result == 1); + Require(ReadAll(exchange.request) == "old body"); + exchange.Done(); + exchange.requestHeaders = {{"Cache-Control", "no-cache"}}; + Require(exchange.Retrieve("http://stale/a") == 0 && exchange.result == 2); + Require(exchange.Revalidate(200, {{"Cache-Control", "max-age=600"}, {"ETag", "e3"}}) == 0 && exchange.result == 0); + Require(exchange.response.headers == nullptr); + Require(sceHttpCacheSetData(exchange.request, "new body", 8) == 0); + Require(sceHttpCacheCompleteRequest(exchange.request) == 0); + exchange.Done(); + exchange.requestHeaders = {{"Pragma", "no-cache"}}; + Require(exchange.Retrieve("http://stale/a") == 0 && exchange.result == 2 && Value(exchange.next, "If-None-Match") == "e3"); + exchange.Done(); + exchange.requestHeaders = {}; + Require(exchange.Retrieve("http://stale/a") == 0 && exchange.result == 1); + Require(ReadAll(exchange.request) == "new body"); + exchange.Done(); + Require(exchange.Retrieve("http://stale/a") == 0 && exchange.result == 1); + Require(exchange.Revalidate(304, {{"Cache-Control", "max-age=60"}}) == 0 && exchange.result == 0); + exchange.Done(); + exchange.requestHeaders = {{"Cache-Control", "no-cache"}}; + Require(exchange.Retrieve("http://stale/a") == 0 && exchange.result == 2); + Require(exchange.Revalidate(304, {{"ETag", "e3"}}) == 0 && exchange.result == 0); + exchange.Done(); + exchange.requestHeaders = {}; + RequireMiss(exchange, "http://stale/a"); + Cached(exchange, "http://stale/b", {{"Cache-Control", "max-age=60"}, {"ETag", "b"}}, "b"); + exchange.requestHeaders = {{"Cache-Control", "no-cache"}}; + Require(exchange.Retrieve("http://stale/b") == 0 && exchange.result == 2); + Require(exchange.Revalidate(404, {{"Cache-Control", "max-age=60"}}) == 0 && exchange.result == 0); + exchange.Done(); + exchange.requestHeaders = {}; + RequireMiss(exchange, "http://stale/b"); + Cached(exchange, "http://stale/c", {{"Cache-Control", "max-age=60"}}, "c"); + Advance(120); + RequireMiss(exchange, "http://stale/c"); + exchange.requestHeaders = {{"Cache-Control", "max-stale"}}; + RequireMiss(exchange, "http://stale/c"); + exchange.requestHeaders = {}; + Cached(exchange, "http://stale/d", {{"Cache-Control", "max-age=60"}}, "d"); + Advance(120); + exchange.requestHeaders = {{"Cache-Control", "max-stale"}}; + Require(exchange.Retrieve("http://stale/d") == 0 && exchange.result == 1); + Require(Value(exchange.response, "Warning") == "110 - \"Response is Stale\"" && ReadAll(exchange.request) == "d"); + exchange.Done(); + exchange.requestHeaders = {{"Cache-Control", "max-stale=30"}}; + RequireMiss(exchange, "http://stale/d"); + exchange.requestHeaders = {}; + Cached(exchange, "http://stale/e", {{"Cache-Control", "max-age=60, stale-while-revalidate=600"}, {"ETag", "e"}}, "swr"); + Advance(120); + Require(exchange.Retrieve("http://stale/e") == 0 && exchange.result == 2); + Require(exchange.response.statusCode == 200 && Value(exchange.response, "Warning") == "110 - \"Response is Stale\""); + Require(Value(exchange.response, "Age").substr(0, 2) == "12" && ReadAll(exchange.request) == "swr"); + exchange.Done(); + Cached(exchange, "http://stale/f", {{"Cache-Control", "max-age=60, stale-if-error=600"}, {"ETag", "f"}}, "sie"); + Advance(120); + Require(exchange.Retrieve("http://stale/f") == 0 && exchange.result == 2 && exchange.response.headers == nullptr); + Require(exchange.Revalidate(503, {}) == 0 && exchange.result == 1); + Require(exchange.response.statusCode == 200 && Value(exchange.response, "Warning") == "110 - \"Response is Stale\"" && ReadAll(exchange.request) == "sie"); + exchange.Done(); + Require(sceHttpCacheTerm(exchange.cache) == 0); +} + +void TestPublicResponses() { + Exchange exchange{sceHttpCacheInit(0)}; + Require(exchange.Retrieve("http://public/a") == 0 && exchange.result == 0); + std::vector
headers{{"Cache-Control", "public, max-age=60"}}; + ResponseHeader header{200, 0, headers.size(), headers.data()}; + Require(sceHttpCacheSetCacheSharing(0) == 0); + Require(sceHttpCacheSetResponseHeader(exchange.request, &header) == 0); + Require(sceHttpCacheCompleteRequest(exchange.request) == 0); + exchange.Done(); + Require(exchange.Retrieve("http://public/a") == 0 && exchange.result == 1); + exchange.Done(); + Require(sceHttpCacheSetCacheSharing(1) == 0); + exchange.user = 0xfe; + Cached(exchange, "http://public/b", headers, "shared"); + Require(exchange.Retrieve("http://public/b") == 0 && exchange.result == 1 && ReadAll(exchange.request) == "shared"); + exchange.Done(); + exchange.user = InitialUser; + Require(exchange.Retrieve("http://public/a") == 0); + exchange.Done(); + Require(sceHttpCacheClearAll(exchange.cache, 0xfe) == 0); + RequireMiss(exchange, "http://public/b"); + Require(sceHttpCacheTerm(exchange.cache) == 0); +} + +} + +int main() { + TestWithoutCache(); + TestRequests(); + TestCacheArguments(); + TestCacheLimit(); + TestStoreAndHit(); + TestLifetimeOrder(); + TestRevalidation(); + TestPublicResponses(); + return 0; +} diff --git a/core/libs/tests/GuestHttpOptions.cpp b/core/libs/tests/GuestHttpOptions.cpp index e396f51ccd..d5f927e1fc 100644 --- a/core/libs/tests/GuestHttpOptions.cpp +++ b/core/libs/tests/GuestHttpOptions.cpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace { @@ -166,6 +167,34 @@ void CheckTermEndsOptions() { Require(sceHttpTerm_nid_postfix(next) == 0, "sceHttpTerm failed"); } +bool RefusesNull(int context, int* netPoolId, int* sslContextId) { + try { + sceHttpGetRegisteredCtxIds(context, netPoolId, sslContextId); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + +void CheckRegisteredCtxIds() { + int netPoolId = -1; + int sslContextId = -1; + Require(sceHttpGetRegisteredCtxIds(UnknownId, &netPoolId, &sslContextId) == BeforeInit, "sceHttpGetRegisteredCtxIds answered before sceHttpInit"); + const int first = sceHttpInit_nid_postfix(3, 7, 0x10000); + const int second = sceHttpInit_nid_postfix(5, 9, 0x20000); + Require(sceHttpGetRegisteredCtxIds(first, &netPoolId, &sslContextId) == 0 && netPoolId == 3 && sslContextId == 7, "the ids given to the first sceHttpInit were not reported"); + Require(sceHttpGetRegisteredCtxIds(second, &netPoolId, &sslContextId) == 0 && netPoolId == 5 && sslContextId == 9, "the ids given to the second sceHttpInit were not reported"); + Require(RefusesNull(second, nullptr, &sslContextId) && RefusesNull(second, &netPoolId, nullptr), "a null output was taken"); + netPoolId = -1; + sslContextId = -1; + Require(sceHttpGetRegisteredCtxIds(UnknownId, &netPoolId, &sslContextId) == InvalidId, "sceHttpGetRegisteredCtxIds took an unknown id"); + Require(sceHttpTerm_nid_postfix(first) == 0, "sceHttpTerm failed"); + Require(sceHttpGetRegisteredCtxIds(first, &netPoolId, &sslContextId) == InvalidId, "a terminated context still reported its ids"); + Require(netPoolId == -1 && sslContextId == -1, "a refused query wrote its outputs"); + Require(sceHttpTerm_nid_postfix(second) == 0, "sceHttpTerm failed"); + Require(sceHttpGetRegisteredCtxIds(second, &netPoolId, &sslContextId) == BeforeInit, "sceHttpGetRegisteredCtxIds answered after the last sceHttpTerm"); +} + void CheckUnobservedOptions(int context) { const int pattern = sceHttpCreateTemplate(context, "anyps5-test", 2, 0); const int connection = sceHttpCreateConnectionWithURL(pattern, "http://example.com/", 1); @@ -206,6 +235,7 @@ int main() { Require(sceHttpTerm_nid_postfix(context) == 0, "sceHttpTerm failed"); Require(sceHttpSetDefaultAcceptEncodingGZIPEnabled(context, 1) == BeforeInit, "a gzip default was taken before sceHttpInit"); CheckTermEndsOptions(); + CheckRegisteredCtxIds(); std::puts("HTTP option tests passed"); return 0; } diff --git a/core/libs/tests/GuestIcuVersion.cpp b/core/libs/tests/GuestIcuVersion.cpp new file mode 100644 index 0000000000..b464003cac --- /dev/null +++ b/core/libs/tests/GuestIcuVersion.cpp @@ -0,0 +1,158 @@ +#include "prx/libSceIcu/libSceIcu.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using Version = std::array; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "ICU version: %s\n", message); + std::abort(); +} + +bool ExpectedInvariant(std::uint32_t unit) { + if (unit > 0x7f) return false; + for (const std::uint32_t variant : {0x0a, 0x21, 0x23, 0x24, 0x40, 0x5b, 0x5c, 0x5d, 0x5e, 0x60, 0x7b, 0x7c, 0x7d, 0x7e}) + if (unit == variant) return false; + return true; +} + +template +bool Rejects(F call) { + try { + call(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + +Version FromString(const char* text) { + Version version{9, 9, 9, 9}; + u_versionFromString_nid_postfix(version.data(), text); + return version; +} + +Version FromUString(const char16_t* text) { + Version version{9, 9, 9, 9}; + u_versionFromUString_nid_postfix(version.data(), text); + return version; +} + +std::string ToString(Version version) { + std::array text; + text.fill('X'); + u_versionToString_nid_postfix(version.data(), text.data()); + const auto length = std::strlen(text.data()); + Require(length <= 15 && text[length + 1] == 'X', "version string overrun"); + return text.data(); +} + +void CharsToUChars() { + std::array chars; + for (std::size_t index = 0; index < chars.size(); ++index) chars[index] = static_cast(index); + std::array units; + units.fill(0x5555); + u_charsToUChars_nid_postfix(chars.data(), units.data(), 256); + for (std::size_t index = 0; index < chars.size(); ++index) Require(units[index] == index, "bytes widen without sign extension"); + Require(units[256] == 0x5555, "widening stays within the length"); + units.fill(0x5555); + u_charsToUChars_nid_postfix(chars.data(), units.data(), 0); + u_charsToUChars_nid_postfix(chars.data(), units.data(), -1); + Require(units[0] == 0x5555, "nonpositive lengths write nothing"); + u_charsToUChars_nid_postfix(nullptr, nullptr, 0); + u_charsToUChars_nid_postfix(nullptr, nullptr, -5); + Require(Rejects([&] { u_charsToUChars_nid_postfix(nullptr, units.data(), 1); }), "null source rejected"); + Require(Rejects([&] { u_charsToUChars_nid_postfix(chars.data(), nullptr, 1); }), "null destination rejected"); +} + +void UCharsToChars() { + std::array units; + for (std::size_t index = 0; index < units.size(); ++index) units[index] = static_cast(index); + std::array chars; + chars.fill('X'); + u_UCharsToChars_nid_postfix(units.data(), chars.data(), 0x10000); + for (std::uint32_t unit = 0; unit < 0x10000; ++unit) + Require(chars[unit] == (ExpectedInvariant(unit) ? static_cast(unit) : 0), "only invariant characters narrow"); + Require(chars[0x10000] == 'X', "narrowing stays within the length"); + chars.fill('X'); + u_UCharsToChars_nid_postfix(units.data(), chars.data(), 0); + u_UCharsToChars_nid_postfix(units.data(), chars.data(), -1); + Require(chars[0] == 'X', "nonpositive lengths write nothing"); + u_UCharsToChars_nid_postfix(nullptr, nullptr, 0); + Require(Rejects([&] { u_UCharsToChars_nid_postfix(nullptr, chars.data(), 1); }), "null source rejected"); + Require(Rejects([&] { u_UCharsToChars_nid_postfix(units.data(), nullptr, 1); }), "null destination rejected"); +} + +void VersionFromString() { + Require(FromString("1.2.3.4") == Version{1, 2, 3, 4}, "four parts"); + Require(FromString("65.1") == Version{65, 1, 0, 0}, "missing parts are zero"); + Require(FromString("1.2.3.4.5") == Version{1, 2, 3, 4}, "parts after the fourth are ignored"); + Require(FromString("") == Version{0, 0, 0, 0}, "empty string"); + Require(FromString("x") == Version{0, 0, 0, 0}, "no digits"); + Require(FromString("3x.4") == Version{3, 0, 0, 0}, "parsing stops at a non-delimiter"); + Require(FromString("1..2") == Version{1, 0, 0, 0}, "empty part stops parsing"); + Require(FromString("1.2.") == Version{1, 2, 0, 0}, "trailing delimiter"); + Require(FromString(" 7. 8") == Version{7, 8, 0, 0}, "leading whitespace is skipped by strtoul"); + Require(FromString("+5.-1") == Version{5, 255, 0, 0}, "signs follow strtoul"); + Require(FromString("256.300.511") == Version{0, 44, 255, 0}, "parts truncate to eight bits"); + Require(FromString("99999999999999999999999.1") == Version{255, 1, 0, 0}, "overflow saturates before truncation"); + Require(FromString(nullptr) == Version{0, 0, 0, 0}, "null string clears the version"); + u_versionFromString_nid_postfix(nullptr, "1.2"); +} + +void VersionFromUString() { + Require(FromUString(u"10.2.1") == Version{10, 2, 1, 0}, "UTF-16 version"); + Require(FromUString(u"") == Version{0, 0, 0, 0}, "empty UTF-16 string"); + Require(FromUString(u"1$2") == Version{1, 0, 0, 0}, "variant character becomes NUL"); + Require(FromUString(u"4.é5") == Version{4, 0, 0, 0}, "non-ASCII character becomes NUL"); + Require(FromUString(u"1.2") == Version{0, 0, 0, 0}, "fullwidth digit is not a digit"); + Require(FromUString((std::u16string(17, u'0') + u"1.2").c_str()) == Version{1, 2, 0, 0}, "twenty units are read"); + Require(FromUString((std::u16string(18, u'0') + u"1.2").c_str()) == Version{1, 0, 0, 0}, "units after the twentieth are ignored"); + Require(FromUString(nullptr) == Version{9, 9, 9, 9}, "null string leaves the version unchanged"); + u_versionFromUString_nid_postfix(nullptr, u"1.2"); +} + +void VersionToString() { + Require(ToString({1, 2, 3, 4}) == "1.2.3.4", "four parts"); + Require(ToString({65, 1, 0, 0}) == "65.1", "trailing zero parts are omitted"); + Require(ToString({7, 0, 0, 0}) == "7.0", "at least two parts"); + Require(ToString({0, 0, 0, 0}) == "0.0", "zero version"); + Require(ToString({1, 0, 0, 5}) == "1.0.0.5", "inner zero parts are kept"); + Require(ToString({0, 0, 1, 0}) == "0.0.1", "leading zero parts are kept"); + Require(ToString({255, 199, 99, 10}) == "255.199.99.10", "multi-digit parts"); + Require(ToString({100, 105, 109, 110}) == "10.15.19.110", "a zero tens digit after the hundreds digit is dropped"); + std::array text{'X', 'X', 'X', 'X'}; + u_versionToString_nid_postfix(nullptr, text.data()); + Require(text[0] == 0 && text[1] == 'X', "null version writes an empty string"); + const Version version{1, 2, 0, 0}; + u_versionToString_nid_postfix(version.data(), 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) { + if (!written(part) || !written(255 - part)) continue; + const Version version{static_cast(part), 3, static_cast(255 - part), 1}; + Require(FromString(ToString(version).c_str()) == version, "string round trip"); + } +} + +} + +int main() { + CharsToUChars(); + UCharsToChars(); + VersionFromString(); + VersionFromUString(); + VersionToString(); + RoundTrip(); + return 0; +} diff --git a/core/libs/tests/GuestImeFilterText.cpp b/core/libs/tests/GuestImeFilterText.cpp index f2f6cf3e11..7c15604512 100644 --- a/core/libs/tests/GuestImeFilterText.cpp +++ b/core/libs/tests/GuestImeFilterText.cpp @@ -61,6 +61,7 @@ void CheckArguments() { Require(Run(0, 5, 16, {u'a'}).code == kInvalidParam, "type 5"); Require(Run(0, 0xff, 16, {u'a'}).code == kInvalidParam, "type 0xff"); Require(Run(0, 0x104, 16, {u'a'}).code == kInvalidParam, "type 0x104"); + Require(Run(0, 0x200, 16, {u'a'}).code == kInvalidParam, "type 0x200"); } void CheckSimpleTypes() { diff --git a/core/libs/tests/GuestImeKeyboard.cpp b/core/libs/tests/GuestImeKeyboard.cpp index 449089bc13..a7fff4e531 100644 --- a/core/libs/tests/GuestImeKeyboard.cpp +++ b/core/libs/tests/GuestImeKeyboard.cpp @@ -28,6 +28,7 @@ static void CheckClosed() { KeyboardInfo info; std::memset(&info, 0xa5, sizeof(info)); Require(sceImeKeyboardGetInfo(0, nullptr) == InvalidAddress, "null info"); + Require(sceImeKeyboardOpen(1, nullptr) == InvalidAddress, "null param"); Require(sceImeKeyboardGetInfo(0, &info) == NotOpened, "info without an open keyboard"); RequireUntouched(info); Require(sceImeKeyboardClose(1) == NotOpened, "close without an open keyboard"); diff --git a/core/libs/tests/GuestImeOpen.cpp b/core/libs/tests/GuestImeOpen.cpp index 04cefbca47..a9ecc94acb 100644 --- a/core/libs/tests/GuestImeOpen.cpp +++ b/core/libs/tests/GuestImeOpen.cpp @@ -217,6 +217,7 @@ void RequireCaretSteps(const std::vector& events, uint32_t direction, void CheckValidation() { Buffers buffers; Require(sceImeOpen_nid_postfix(nullptr, nullptr) == InvalidAddress, "null param"); + Require(sceImeUpdate(nullptr) == InvalidHandler, "null handler"); auto expect = [&](auto change, int error, const char* message) { Param param = MakeParam(buffers, 16); change(param); diff --git a/core/libs/tests/GuestImeParameters.cpp b/core/libs/tests/GuestImeParameters.cpp index 82ec0959f8..e0da2559d5 100644 --- a/core/libs/tests/GuestImeParameters.cpp +++ b/core/libs/tests/GuestImeParameters.cpp @@ -108,7 +108,9 @@ static void CheckClosedPanel() { Require(sceImeSetCaret(&caret) == notOpened, "caret needs an open panel"); Require(sceImeSetCaret(nullptr) == notOpened, "caret checks the panel first"); Require(sceImeSetText(text, 4) == notOpened, "text needs an open panel"); + Require(sceImeSetText(nullptr, 4) == notOpened, "null text checks open panel first"); Require(sceImeSetTextGeometry(TextAreaMode::Edit, &geometry) == notOpened, "geometry needs an open panel"); + Require(sceImeSetTextGeometry(TextAreaMode::Edit, nullptr) == notOpened, "null geometry checks open panel first"); Require(sceImeClose_nid_postfix() == notOpened, "closing needs an open panel"); PositionAndForm form; std::memset(&form, 0xa5, sizeof(form)); diff --git a/core/libs/tests/GuestJobManager.cpp b/core/libs/tests/GuestJobManager.cpp index 7690957c0b..af4995720a 100644 --- a/core/libs/tests/GuestJobManager.cpp +++ b/core/libs/tests/GuestJobManager.cpp @@ -1,6 +1,6 @@ #include "prx/libkernel/include/Api.hpp" #include "prx/libkernel/Time/include/Time.hpp" -#include "prx/libSceJobManager/include/Api.hpp" +#include "prx/libSceJobManager/libSceJobManager.hpp" #include "SceTypes.hpp" #include #include @@ -89,6 +89,46 @@ static void MemoryCase() { static void APS5_VABI Nothing(void*, std::uint32_t) { } +static void OwnedStateCase() { + for (const auto [workers, count] : {std::pair{1u, 1u}, std::pair{3u, 32u}, std::pair{13u, 2u}}) { + const auto size = SizeFor(workers, count, count * 4, count * 2, count * 2); + Block block(size + 256); + std::memset(block.memory, 0xa5, block.bytes); + ManagerObject manager; + const auto begin = reinterpret_cast(block.memory); + const auto inside = [begin, size](const void* pointer) { + const auto address = reinterpret_cast(pointer); + return address >= begin && address < begin + size; + }; + for (int round = 0; round < 3; ++round) { + Require(Initialize(manager.storage, block.memory, size, workers, count, nullptr) == 0); + void* implementation = nullptr; + std::memcpy(&implementation, manager.storage, sizeof(implementation)); + Require(inside(implementation)); + for (std::uint32_t priority = 0; priority < 2; ++priority) { + Require(inside(Factories(manager.storage, priority))); + Require(inside(UnifiedFactories(manager.storage, priority))); + } + auto* factory = Factories(manager.storage, round % 2); + std::array sequences{}; + for (std::uint32_t index = 0; index < count; ++index) { + auto*& sequence = sequences[index]; + Require(factory->Create(&sequence, 4, 4, 2, 2, nullptr) == 0 && inside(sequence)); + Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == 0); + Require(sequence->SyncPoint() == 0); + Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == 0); + } + for (std::uint32_t index = count; index != 0; --index) { + Require(sequences[index - 1]->Wait() == 0); + Require(factory->Destroy(sequences[index - 1]) == 0); + } + Require(Shutdown(manager.storage) == 0); + const auto* bytes = static_cast(block.memory); + for (auto offset = size; offset < block.bytes; ++offset) Require(bytes[offset] == 0xa5); + } + } +} + static void ValidationCase() { Block block(SizeFor(2, 2, 16, 4, 4)); ManagerObject manager; @@ -630,11 +670,22 @@ static SpinSample MeasureWorkerSpin(std::uint32_t spin, std::uint32_t rounds, bo return {consumed, clock.reference}; } +static SpinSample QuietestWorkerSpin(std::uint32_t spin, std::uint32_t rounds, bool pending) { + auto best = MeasureWorkerSpin(spin, rounds, pending); + for (int attempt = 1; attempt < 3; ++attempt) { + const auto sample = MeasureWorkerSpin(spin, rounds, pending); + if (sample.consumed < best.consumed) { + best = sample; + } + } + return best; +} + static void WorkerSpinCase() { constexpr std::uint32_t rounds = 512; const auto parked = MeasureWorkerSpin(0, rounds, true); const auto spinning = MeasureWorkerSpin(50, rounds, true); - const auto noPending = MeasureWorkerSpin(50, rounds, false); + const auto noPending = QuietestWorkerSpin(50, rounds, false); #ifdef _WIN32 constexpr const char* units = "cycles"; #else @@ -698,9 +749,150 @@ static void WorkerCountCase() { Require(Shutdown(manager.storage) == 0); } +static void APS5_VABI ShutdownGateJob(void* argument, std::uint32_t index) { + GateJob(argument, index); + ++static_cast(argument)->next; +} + +static void ShutdownActiveCase(bool wait, bool queue, bool destroy, std::uint32_t count = 1) { + for (int round = 0; round < 3; ++round) { + Block block(SizeFor(1, 1, 4, 2, 2)); + ManagerObject manager; + Require(Initialize(manager.storage, block.memory, block.bytes, 1, 1, nullptr) == 0); + ISequenceFactory* factory = Factories(manager.storage, 0); + ISequence* sequence = nullptr; + Require(factory->Create(&sequence, 4, 4, 2, 2, "shutdown active") == 0); + Order order; + Require(sequence->Dispatch(ShutdownGateJob, &order, count, nullptr) == 0); + while (!order.started) std::this_thread::yield(); + if (queue) { + Require(sequence->SyncPoint() == 0); + Require(sequence->Dispatch(LowJob, &order, 1, nullptr) == 0); + } + std::atomic entered{false}; + std::atomic returned{false}; + std::thread waiter; + if (wait) { + waiter = std::thread([&] { + entered = true; + Require(destroy ? factory->Destroy(sequence) == 0 : sequence->Wait() == 0); + returned = true; + }); + while (!entered) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + Require(!returned); + } + std::atomic stopping{false}; + std::atomic stopped{false}; + std::thread shutdown([&] { + stopping = true; + Require(Shutdown(manager.storage) == 0); + stopped = true; + }); + while (!stopping) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + Require(!stopped); + order.release = true; + shutdown.join(); + if (waiter.joinable()) waiter.join(); + Require(stopped && (!wait || returned)); + Require(order.next == 1); + Require(Factories(manager.storage, 0) == nullptr); + Require(Initialize(manager.storage, block.memory, block.bytes, 1, 1, nullptr) == 0); + Require(Shutdown(manager.storage) == 0); + } +} + +struct ShutdownCrossWait { + ISequence* sequence; + std::atomic entered{false}; + std::atomic returned{false}; +}; + +static void APS5_VABI ShutdownWaitOther(void* argument, std::uint32_t) { + auto& state = *static_cast(argument); + state.entered = true; + Require(state.sequence->Wait() == 0); + state.returned = true; +} + +static void ConcurrentWaitCase() { + for (const bool stop : {false, true}) { + for (int round = 0; round < 3; ++round) { + Block block(SizeFor(2, 1, 4, 2, 8)); + ManagerObject manager; + Require(Initialize(manager.storage, block.memory, block.bytes, 2, 1, nullptr) == 0); + auto* factory = Factories(manager.storage, 0); + ISequence* sequence = nullptr; + Require(factory->Create(&sequence, 2, 2, 1, 8, nullptr) == 0); + Order order; + Require(sequence->Dispatch(ShutdownGateJob, &order, 1, nullptr) == 0); + while (!order.started) std::this_thread::yield(); + std::atomic entered{0}; + std::atomic returned{0}; + std::array waiters; + for (auto& waiter : waiters) waiter = std::thread([&] { + ++entered; + Require(sequence->Wait() == 0); + ++returned; + }); + while (entered != waiters.size()) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(returned == 0); + std::thread shutdown; + if (stop) { + shutdown = std::thread([&] { Require(Shutdown(manager.storage) == 0); }); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + order.release = true; + for (auto& waiter : waiters) waiter.join(); + Require(returned == waiters.size() && order.next == 1); + if (stop) shutdown.join(); + else { + Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == 0); + Require(factory->Destroy(sequence) == 0); + Require(Shutdown(manager.storage) == 0); + } + } + } +} + +static void ShutdownCrossWaitCase() { + Block block(SizeFor(2, 2, 4, 2, 2)); + ManagerObject manager; + Require(Initialize(manager.storage, block.memory, block.bytes, 2, 2, nullptr) == 0); + auto* factory = Factories(manager.storage, 0); + ISequence* producer = nullptr; + ISequence* consumer = nullptr; + Require(factory->Create(&producer, 1, 1, 1, 1, nullptr) == 0); + Require(factory->Create(&consumer, 1, 1, 1, 1, nullptr) == 0); + Order order; + Require(producer->Dispatch(ShutdownGateJob, &order, 1, nullptr) == 0); + while (!order.started) std::this_thread::yield(); + ShutdownCrossWait state{producer}; + Require(consumer->Dispatch(ShutdownWaitOther, &state, 1, nullptr) == 0); + while (!state.entered) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Require(!state.returned); + std::atomic stopping{false}; + std::atomic stopped{false}; + std::thread shutdown([&] { + stopping = true; + Require(Shutdown(manager.storage) == 0); + stopped = true; + }); + while (!stopping) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Require(!stopped); + order.release = true; + shutdown.join(); + Require(state.returned && stopped && order.next == 1); +} + int RunJobManagerCase(const char* caseName) { const std::string name = caseName; if (name == "memory") MemoryCase(); + else if (name == "owned_state") OwnedStateCase(); else if (name == "validation") ValidationCase(); else if (name == "dispatch") DispatchCase(); else if (name == "ordering") OrderingCase(); @@ -714,6 +906,13 @@ int RunJobManagerCase(const char* caseName) { else if (name == "worker_wake") WorkerWakeCase(); else if (name == "worker_spin") WorkerSpinCase(); else if (name == "worker_count") WorkerCountCase(); + else if (name == "shutdown_running") ShutdownActiveCase(false, false, false); + else if (name == "shutdown_waiter") ShutdownActiveCase(true, false, false); + else if (name == "shutdown_queued") ShutdownActiveCase(true, true, false); + else if (name == "shutdown_destroy") ShutdownActiveCase(true, true, true); + else if (name == "shutdown_partial") ShutdownActiveCase(true, false, false, 4); + else if (name == "shutdown_cross_wait") ShutdownCrossWaitCase(); + else if (name == "concurrent_wait") ConcurrentWaitCase(); else return 2; return 0; } diff --git a/core/libs/tests/GuestJxrLookup.cpp b/core/libs/tests/GuestJxrLookup.cpp new file mode 100644 index 0000000000..fea9861f98 --- /dev/null +++ b/core/libs/tests/GuestJxrLookup.cpp @@ -0,0 +1,282 @@ +#include "prx/libSceJxr/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + +extern "C" int JxrReferenceCase_nid_no_patch(std::uint8_t, int, int, std::uint8_t*, std::uint64_t*); +using Query = decltype(&PixelFormatLookup_nid_postfix); +static_assert(sizeof(JxrPixelInfo) == 56 && alignof(JxrPixelInfo) == 8); +static_assert(offsetof(JxrPixelInfo, guid) == 0 && offsetof(JxrPixelInfo, channels) == 8); +static_assert(offsetof(JxrPixelInfo, colorFormat) == 16 && offsetof(JxrPixelInfo, bitDepth) == 20); +static_assert(offsetof(JxrPixelInfo, bitsPerUnit) == 24 && offsetof(JxrPixelInfo, flags) == 32); +static_assert(offsetof(JxrPixelInfo, interpretation) == 40 && offsetof(JxrPixelInfo, sampleFormat) == 52); + +namespace { + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "JXR lookup: %s\n", message); + std::exit(1); +} + +struct Reference { + std::array guid{}; + std::array fields{}; +}; + +bool ReadReference(unsigned hash, bool reverse, bool white, Reference& expected) { + const auto result = JxrReferenceCase_nid_no_patch(static_cast(hash), reverse, white, + expected.guid.data(), expected.fields.data()); + Require(result >= 0, "native lookup reference failed"); + return result != 0; +} + +JxrPixelInfo BackwardInput(const Reference& source) { + JxrPixelInfo input{}; + input.guid = reinterpret_cast(std::uintptr_t{1}); + input.channels = UINT64_MAX; + input.colorFormat = -1; + input.bitDepth = -1; + input.bitsPerUnit = UINT32_MAX; + input.flags = source.fields[4] | UINT64_C(0xfedcba9880000040); + input.interpretation = static_cast(source.fields[5]); + input.samplesPerPixel = static_cast(source.fields[6]); + input.bitsPerSample = static_cast(source.fields[7]); + input.sampleFormat = static_cast(source.fields[8]); + return input; +} + +void CheckValue(const JxrPixelInfo& actual, const Reference& expected) { + const std::array fields{{actual.channels, static_cast(actual.colorFormat), + static_cast(actual.bitDepth), actual.bitsPerUnit, actual.flags, actual.interpretation, + actual.samplesPerPixel, actual.bitsPerSample, actual.sampleFormat}}; + Require(fields == expected.fields, "metadata differs from pinned native reference"); + Require(actual.guid != nullptr, "successful lookup returned null GUID"); + Require(std::memcmp(actual.guid, expected.guid.data(), 16) == 0, "GUID differs from pinned native reference"); +} + +void PaddingCases(Query query, bool reverse) { + constexpr auto begin = offsetof(JxrPixelInfo, bitsPerUnit) + sizeof(JxrPixelInfo::bitsPerUnit); + constexpr auto size = offsetof(JxrPixelInfo, flags) - begin; + static_assert(begin == 28 && size == 4); + const std::array zero{}; + struct Guarded { + std::uint64_t before; + JxrPixelInfo value; + std::uint64_t after; + }; + unsigned formats = 0; + for (unsigned hash = 0; hash < 256; ++hash) { + Reference forward; + if (!ReadReference(hash, false, false, forward)) continue; + Reference expected = forward; + Guarded record; + std::memset(&record, 0xa5, sizeof(record)); + if (reverse) { + record.value = BackwardInput(forward); + Require(ReadReference(hash, true, false, expected), "padding reverse reference missing"); + } else { + record.value.guid = forward.guid.data(); + } + auto* bytes = reinterpret_cast(&record.value); + std::memset(bytes + begin, 0xa5, size); + Require(query(&record.value, reverse ? 1 : 0) == 0, "padding lookup succeeds"); + CheckValue(record.value, expected); + Require(std::memcmp(bytes + begin, zero.data(), size) == 0, + reverse ? "reverse success padding is not zero" : "forward success padding is not zero"); + Require(record.before == UINT64_C(0xa5a5a5a5a5a5a5a5) && record.after == UINT64_C(0xa5a5a5a5a5a5a5a5), + "padding lookup changed record canaries"); + ++formats; + } + Require(formats != 0, "padding reference inventory empty"); + std::printf("JXR %s padding passed across %u reference hashes\n", reverse ? "reverse" : "forward", formats); +} + +class GuardedTail { + void* memory; + std::size_t page; + void* data; +public: + GuardedTail(const void* source, std::size_t bytes) { +#ifdef _WIN32 + SYSTEM_INFO info; + GetSystemInfo(&info); + page = info.dwPageSize; + memory = VirtualAlloc(nullptr, page * 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + Require(memory != nullptr, "guard allocation"); + DWORD previous; + Require(VirtualProtect(static_cast(memory) + page, page, PAGE_NOACCESS, &previous) != 0, "guard protection"); +#else + page = static_cast(sysconf(_SC_PAGESIZE)); + memory = mmap(nullptr, page * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(memory != MAP_FAILED, "guard allocation"); + Require(mprotect(static_cast(memory) + page, page, PROT_NONE) == 0, "guard protection"); +#endif + Require(bytes <= page, "guard fixture size"); + data = static_cast(memory) + page - bytes; + std::memcpy(data, source, bytes); + } + void ReadOnly() { +#ifdef _WIN32 + DWORD previous; + Require(VirtualProtect(memory, page, PAGE_READONLY, &previous) != 0, "read-only protection"); +#else + Require(mprotect(memory, page, PROT_READ) == 0, "read-only protection"); +#endif + } + ~GuardedTail() { +#ifdef _WIN32 + VirtualFree(memory, 0, MEM_RELEASE); +#else + munmap(memory, page * 2); +#endif + } + template T* As() const { return static_cast(data); } +}; + +void Cases(Query query) { + Require(query(nullptr, 2) == -106, "invalid mode has signed 64-bit unsupported-format result"); + Reference forward; + unsigned formats = 0; + for (unsigned hash = 0; hash < 256; ++hash) { + if (!ReadReference(hash, false, false, forward)) continue; + ++formats; + JxrPixelInfo input; + std::memset(&input, 0xa5, sizeof(input)); + input.guid = forward.guid.data(); + const auto originalGuid = forward.guid; + Require(query(&input, 0) == 0, "forward lookup"); + CheckValue(input, forward); + Require(forward.guid == originalGuid, "lookup modified input GUID"); + Reference backward; + Require(ReadReference(hash, true, false, backward), "backward native case missing"); + input = BackwardInput(forward); + Require(query(&input, 1) == 0, "backward lookup ignores GUID and high flag bits"); + CheckValue(input, backward); + } + Require(formats != 0, "reference format inventory empty"); + Reference white; + Require(ReadReference(5, false, false, forward), "black-white reference"); + Require(ReadReference(5, true, true, white), "white-is-zero reference"); + Require(forward.fields[5] == 1 && white.fields[5] == 0, "duplicate GUID reference order"); + auto input = BackwardInput(forward); + input.interpretation = 0; + Require(query(&input, 1) == 0, "backward white-is-zero lookup"); + CheckValue(input, white); + Require(query(&input, 0) == 0, "forward lookup selects first duplicate GUID"); + CheckValue(input, forward); + for (unsigned mode = 2; mode < 256; ++mode) { + std::memset(&input, 0xa5, sizeof(input)); + std::array before; + std::memcpy(before.data(), &input, sizeof(input)); + Require(query(&input, static_cast(mode)) == -106, "invalid mode error"); + Require(std::memcmp(&input, before.data(), sizeof(input)) == 0, "invalid mode changed record"); + Require(query(reinterpret_cast(std::uintptr_t{1}), static_cast(mode)) == -106, + "invalid mode must not read inaccessible record"); + } + std::array unknown{}; + std::memset(&input, 0xff, sizeof(input)); + input.guid = unknown.data(); + std::array before; + std::memcpy(before.data(), &input, sizeof(input)); + Require(query(&input, 0) == -106 && std::memcmp(&input, before.data(), sizeof(input)) == 0, "unknown GUID unchanged"); + Require(query(&input, 1) == -106 && std::memcmp(&input, before.data(), sizeof(input)) == 0, "unknown TIFF fields unchanged"); + GuardedTail failedRecord(&input, sizeof(input)); + failedRecord.ReadOnly(); + Require(query(failedRecord.As(), 0) == -106, "failed lookup does not write read-only record"); + const void* unknownPointer = unknown.data(); + GuardedTail failedPrefix(&unknownPointer, sizeof(unknownPointer)); + failedPrefix.ReadOnly(); + Require(query(failedPrefix.As(), 0) == -106, "failed forward lookup only reads GUID pointer"); + Require(ReadReference(8, false, false, forward), "Gray8 reference"); + Require(forward.fields[0] == 1 && forward.fields[7] == 8, "Gray8 reference values"); + GuardedTail guid(forward.guid.data(), 16); + guid.ReadOnly(); + input = {}; + input.guid = guid.As(); + GuardedTail record(&input, sizeof(input)); + Require(query(record.As(), 0) == 0, "bounded record and read-only GUID"); + CheckValue(*record.As(), forward); + input = BackwardInput(forward); + input.guid = nullptr; + Require(query(&input, 1) == 0, "backward lookup ignores null GUID"); + CheckValue(input, forward); + alignas(16) std::array unalignedGuid{}; + std::memcpy(unalignedGuid.data() + 1, forward.guid.data(), 16); + input = {}; + input.guid = unalignedGuid.data() + 1; + Require(query(&input, 0) == 0, "unaligned GUID lookup"); + CheckValue(input, forward); + const auto borrowedGuid = record.As()->guid; + std::array threads; + for (auto& thread : threads) thread = std::thread([&] { + for (unsigned i = 0; i < 32; ++i) { + JxrPixelInfo value{}; + value.guid = forward.guid.data(); + Require(query(&value, 0) == 0, "concurrent independent lookup"); + CheckValue(value, forward); + Require(value.guid == borrowedGuid, "borrowed GUID pointer changed across calls"); + } + }); + for (auto& thread : threads) thread.join(); + std::printf("JXR guest lookup cases passed across %u reference hashes\n", formats); +} + +} + +int main(int argc, char** argv) { +#ifdef _WIN32 + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); +#endif + try { + if (argc == 2 && std::strcmp(argv[1], "reference") == 0) { + Reference gray; + Require(ReadReference(8, false, false, gray) && gray.fields[0] == 1 && gray.fields[7] == 8, "native Gray8 reference"); + unsigned formats = 0; + for (unsigned hash = 0; hash < 256; ++hash) { Reference item; if (ReadReference(hash, false, false, item)) ++formats; } + std::printf("Pinned native JXR reference passed with %u hashes\n", formats); + return 0; + } + Require(argc == 3 || argc == 4, "expected library, export mode and optional padding direction"); + const char* name = std::strcmp(argv[2], "patched") == 0 ? "5Gjz1nLOKvo" : "PixelFormatLookup_nid_postfix"; +#ifdef _WIN32 + const auto library = LoadLibraryA(argv[1]); + Require(library != nullptr, "load requested library"); + const auto query = reinterpret_cast(GetProcAddress(library, name)); +#else + const auto library = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL); + Require(library != nullptr, "load requested library"); + const auto query = reinterpret_cast(dlsym(library, name)); +#endif + Require(query != nullptr, "missing lookup export"); + if (argc == 4) { + Require(std::strcmp(argv[3], "forward") == 0 || std::strcmp(argv[3], "reverse") == 0, "padding direction"); + PaddingCases(query, std::strcmp(argv[3], "reverse") == 0); + } else { + Cases(query); + } +#ifdef _WIN32 + FreeLibrary(library); +#else + dlclose(library); +#endif + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "JXR lookup exception: %s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestJxrLookupReference.c b/core/libs/tests/GuestJxrLookupReference.c new file mode 100644 index 0000000000..46d61a89e1 --- /dev/null +++ b/core/libs/tests/GuestJxrLookupReference.c @@ -0,0 +1,31 @@ +#include "strcodec.h" +#include "JXRGlue.h" +#include +#include + +_Static_assert(sizeof(GUID) == 16, "GUID size"); +_Static_assert(sizeof(U32) == 4, "U32 size"); + +int JxrReferenceCase_nid_no_patch(uint8_t hash, int reverse, int white, uint8_t* guid, uint64_t* fields) { + PKPixelInfo native = {0}; + native.pGUIDPixFmt = GetPixelFormatFromHash(hash); + if (!native.pGUIDPixFmt) return 0; + if (PixelFormatLookup(&native, LOOKUP_FORWARD) != 0) return -1; + if (reverse) { + native.pGUIDPixFmt = NULL; + native.grBit |= PK_pixfmtBGR | PK_pixfmtNeedConvert; + if (white) native.uInterpretation = 0; + if (PixelFormatLookup(&native, LOOKUP_BACKWARD_TIF) != 0) return -1; + } + memcpy(guid, native.pGUIDPixFmt, 16); + fields[0] = native.cChannel; + fields[1] = native.cfColorFormat; + fields[2] = native.bdBitDepth; + fields[3] = native.cbitUnit; + fields[4] = native.grBit; + fields[5] = native.uInterpretation; + fields[6] = native.uSamplePerPixel; + fields[7] = native.uBitsPerSample; + fields[8] = native.uSampleFormat; + return 1; +} diff --git a/core/libs/tests/GuestKernelAppArchive.cpp b/core/libs/tests/GuestKernelAppArchive.cpp index f09c256ba8..d3922949f5 100644 --- a/core/libs/tests/GuestKernelAppArchive.cpp +++ b/core/libs/tests/GuestKernelAppArchive.cpp @@ -1,21 +1,33 @@ #include "prx/libkernel/include/Api.hpp" -#include "prx/libkernel/File/include/File.hpp" #include "prx/libc/include/Api.hpp" +#include "prx/libSceLibcInternal/include/Api.hpp" #include "AppArchiveFixture.hpp" #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestFileNumber.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/File/include/KernelStatfs.hpp" #include "prx/libkernel/KernelErrors.hpp" +#include "prx/libc/include/GuestDirectory.hpp" #include +#include +#include +#include #include #include #include #include #include #include +#include +#include #include #include +#include +#include #include #include @@ -27,6 +39,15 @@ using TerminateHandler = void (*)(); namespace { +struct KernelLock { + std::int64_t start; + std::int64_t length; + std::int32_t pid; + std::int16_t type; + std::int16_t whence; + std::int32_t system; +}; + void Require(bool value, int line) { if (!value) { std::fprintf(stderr, "app0.zar kernel check failed at line %d\n", line); @@ -60,14 +81,240 @@ constexpr int DuplicateFrom = 0; constexpr std::uint64_t ReadableRequest = 0x4004667f; void Archive(const std::filesystem::path& root) { - AppArchiveFixture::WriteArchive(root / "app0.zar", { + std::map> files{ {"Data/Big.bin", AppArchiveFixture::Pattern(BigSize)}, {"Data/Small.txt", {'z', 'a', 'r'}}, {"sce_sys/Packed.json", {'{', '}'}}, {"eboot.bin", {'o', 'l', 'd'}}, - }); + {"Readme.TXT", {'z'}}, + {"MaskedDir/Child.txt", {'d'}}, + {"MaskedFile", {'f'}}, + }; + for (unsigned index = 0; index < 32; ++index) { + const auto suffix = std::string(1, static_cast('a' + index / 10)) + static_cast('0' + index % 10); + files.emplace("sce_sys/Concurrent/" + suffix, std::vector{static_cast(index)}); + } + AppArchiveFixture::WriteArchive(root / "app0.zar", files, {"EmptyArchive"}); std::ofstream eboot(root / "app0" / "eboot.bin", std::ios::binary); eboot << "disk"; + std::ofstream readme(root / "app0" / "README.txt", std::ios::binary); + readme << "disk"; + std::ofstream masked(root / "app0" / "MaskedDir", std::ios::binary); + masked << "disk"; + std::filesystem::create_directory(root / "app0" / "MaskedFile"); +} + +void FileNumbers() { + constexpr std::uint32_t Fold = 0x9E3779B1u; + Check(GuestFileNumber_nid_no_patch(7) == 7); + Check(GuestFileNumber_nid_no_patch((std::uint64_t{1} << 32) | 7) == (Fold ^ 7u)); + Check(GuestFileNumber_nid_no_patch((std::uint64_t{1} << 32) | Fold) == 1); +} + +void ConcurrentArchiveDirectoryEntries() { + constexpr int WorkerCount = 8; + constexpr int ChildCount = 32; + struct Entry { + std::int64_t base; + std::string name; + }; + const int directory = sceKernelOpen("/app0/sce_sys/Concurrent", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Check(directory >= 0); + std::barrier start(WorkerCount); + std::mutex entriesMutex; + std::vector entries; + entries.reserve(ChildCount + 2); + std::atomic failures{0}; + std::vector workers; + workers.reserve(WorkerCount); + for (int worker = 0; worker < WorkerCount; ++worker) { + workers.emplace_back([&] { + start.arrive_and_wait(); + for (;;) { + std::array record{}; + std::int64_t base = -1; + const int bytes = sceKernelGetdirentries(directory, record.data(), static_cast(record.size()), &base); + if (bytes == 0) break; + if (bytes != static_cast(record.size()) || base < 0 || record[7] == 0) { + failures.fetch_add(1, std::memory_order_relaxed); + break; + } + std::lock_guard lock(entriesMutex); + entries.push_back({base, std::string(record.data() + 8)}); + } + }); + } + for (auto& worker : workers) worker.join(); + Check(failures.load(std::memory_order_relaxed) == 0); + Check(sceKernelClose(directory) == 0); + std::sort(entries.begin(), entries.end(), [](const Entry& left, const Entry& right) { return left.base < right.base; }); + Check(entries.size() == ChildCount + 2); + Check(entries[0].base == 0 && entries[0].name == "."); + Check(entries[1].base == 1 && entries[1].name == ".."); + for (int index = 0; index < ChildCount; ++index) { + const auto name = std::string(1, static_cast('a' + index / 10)) + static_cast('0' + index % 10); + Check(entries[index + 2].base == index + 2 && entries[index + 2].name == name); + } +} + +std::map ReadDirectory(void* directory) { + std::map entries; + while (auto* entry = readdir_nid_postfix(directory)) Check(entries.emplace(entry->name, entry->type).second); + return entries; +} + +std::map ReadKernelDirectory(int fd) { + std::array bytes{}; + std::map entries; + const int count = sceKernelGetdents(fd, bytes.data(), static_cast(bytes.size())); + Check(count >= 0); + for (int offset = 0; offset < count;) { + std::uint16_t recordLength = 0; + std::memcpy(&recordLength, bytes.data() + offset + 4, sizeof(recordLength)); + const auto nameLength = static_cast(bytes[offset + 7]); + const std::string name(bytes.data() + offset + 8); + Check(name.size() == nameLength && recordLength == ((8 + nameLength + 1 + 3) & ~3)); + Check(offset + recordLength <= count && entries.emplace(name, static_cast(bytes[offset + 6])).second); + offset += recordLength; + } + return entries; +} + +void Directories() { + void* archive = opendir_nid_postfix("/app0/Data"); + Check(archive != nullptr); + const auto archiveEntries = ReadDirectory(archive); + Check(archiveEntries.size() == 4 && archiveEntries.at(".") == 4 && archiveEntries.at("..") == 4); + Check(archiveEntries.at("Big.bin") == 8 && archiveEntries.at("Small.txt") == 8); + GuestDirectoryEntry copied{}; + GuestDirectoryEntry* copiedResult = nullptr; + rewinddir_nid_postfix(archive); + Check(readdir_r_nid_postfix(archive, &copied, &copiedResult) == 0 && copiedResult == &copied && std::strcmp(copied.name, ".") == 0); + rewinddir_nid_postfix(archive); + Check(telldir_nid_postfix(archive) == 0 && readdir_nid_postfix(archive) != nullptr); + Check(telldir_nid_postfix(archive) == 1); + seekdir_nid_postfix(archive, 0); + Check(readdir_nid_postfix(archive) != nullptr); + rewinddir_nid_postfix(archive); + Check(closedir_nid_postfix(archive) == 0); + + void* merged = opendir_nid_postfix("/app0"); + Check(merged != nullptr); +#ifdef _WIN32 + bool diskIdentityChecked = false; + while (auto* entry = readdir_nid_postfix(merged)) { + if (std::strcmp(entry->name, "README.txt") != 0) continue; + FileStat identity{}; + Check(sceKernelStat("/app0/README.txt", &identity) == 0); + Check(entry->fileNumber != 0 && entry->fileNumber == static_cast(identity.st_ino)); + diskIdentityChecked = true; + } + Check(diskIdentityChecked); + rewinddir_nid_postfix(merged); +#endif + const auto mergedEntries = ReadDirectory(merged); + Check(mergedEntries.at("eboot.bin") == 8 && mergedEntries.at("README.txt") == 8); + Check(mergedEntries.at("EmptyArchive") == 4 && mergedEntries.at("Data") == 4); + Check(mergedEntries.at("MaskedDir") == 8 && mergedEntries.at("MaskedFile") == 4); + Check(mergedEntries.size() == 9); + Check(closedir_nid_postfix(merged) == 0); + + void* refreshed = opendir_nid_postfix("/app0"); + Check(refreshed != nullptr && readdir_nid_postfix(refreshed) != nullptr); + const auto addedPath = std::filesystem::current_path() / "app0" / "AfterOpen.txt"; + { std::ofstream added(addedPath); added << "disk"; } + rewinddir_nid_postfix(refreshed); + const auto refreshedEntries = ReadDirectory(refreshed); + Check(refreshedEntries.contains("AfterOpen.txt")); + Check(closedir_nid_postfix(refreshed) == 0); + std::filesystem::remove(addedPath); + + const int fd = sceKernelOpen("/app0/Data", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Check(fd >= 0); + FileStat pathStatus{}; + FileStat descriptorStatus{}; + Check(sceKernelStat("/app0/Data", &pathStatus) == 0 && sceKernelFstat(fd, &descriptorStatus) == 0); + Check((descriptorStatus.st_mode & TypeMask) == DirectoryType && descriptorStatus.st_ino == pathStatus.st_ino && descriptorStatus.st_dev == pathStatus.st_dev); + char rejectedByte = 0; + Check(sceKernelRead(fd, &rejectedByte, 1) == SCE_KERNEL_ERROR_EISDIR); + Check(sceKernelWrite(fd, &rejectedByte, 1) == SCE_KERNEL_ERROR_EISDIR); + Check(sceKernelPread(fd, &rejectedByte, 1, 0) == SCE_KERNEL_ERROR_EISDIR); + const KernelIovec rejectedIovec{&rejectedByte, 1}; + Check(sceKernelReadv(fd, &rejectedIovec, 1) == SCE_KERNEL_ERROR_EISDIR); + Check(sceKernelPreadv(fd, &rejectedIovec, 1, 0) == SCE_KERNEL_ERROR_EISDIR); + Check(sceKernelFsync(fd) == SCE_KERNEL_ERROR_EISDIR && sceKernelFdatasync(fd) == SCE_KERNEL_ERROR_EISDIR); + Check(ftruncate_nid_postfix(fd, 0) == -1 && *__error_nid_postfix() == 22); + Check(ftruncate_nid_postfix(fd, 8) == -1 && *__error_nid_postfix() == 22); + Check(sceKernelFchmod(fd, 0600) == SCE_KERNEL_ERROR_EISDIR); + Check(sceKernelFutimes(fd, nullptr) == SCE_KERNEL_ERROR_EISDIR); + Check(flock_nid_postfix(fd, 1) == -1); + KernelLock recordLock{}; + Check(fcntl_nid_postfix(fd, 12, &recordLock) == -1); + Check(_fstatfs_nid_postfix(fd, nullptr) == -1); + int available = 0; + Check(ioctl_nid_postfix(fd, ReadableRequest, &available) == -1); + Check(mmap_nid_postfix(nullptr, 4096, 1, 1, fd, 0) == reinterpret_cast(static_cast(-1))); + std::array first{}; + Check(sceKernelGetdents(fd, first.data(), 1) == SCE_KERNEL_ERROR_EINVAL); + Check(sceKernelGetdents(fd, first.data(), static_cast(first.size())) == 12); + Check(std::string(first.data() + 8) == "."); + std::uint32_t dotNumber = 0; + std::memcpy(&dotNumber, first.data(), sizeof(dotNumber)); + Check(dotNumber == static_cast(descriptorStatus.st_ino)); + const int duplicate = dup_nid_postfix(fd); + Check(duplicate >= 0); + std::array second{}; + Check(sceKernelGetdents(duplicate, second.data(), static_cast(second.size())) == 12); + Check(std::string(second.data() + 8) == ".."); + FileStat parentStatus{}; + std::uint32_t dotdotNumber = 0; + std::memcpy(&dotdotNumber, second.data(), sizeof(dotdotNumber)); + Check(sceKernelStat("/app0", &parentStatus) == 0 && dotdotNumber == static_cast(parentStatus.st_ino)); + Check(sceKernelLseek(fd, 0, 1) == 2); + Check(sceKernelClose(fd) == 0); + const auto remaining = ReadKernelDirectory(duplicate); + Check(remaining.size() == 2 && remaining.contains("Big.bin") && remaining.contains("Small.txt")); + Check(sceKernelClose(duplicate) == 0); + + const int reused = sceKernelOpen("/app0/Data", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Check(reused >= 0 && File::IsDirectoryDescriptor(reused)); + Check(sceKernelClose(reused) == 0); + const int regular = sceKernelOpen("/app0/eboot.bin", SCE_KERNEL_O_RDONLY, 0); + Check(regular == reused && !File::IsDirectoryDescriptor(regular)); + Check(sceKernelFstat(regular, &descriptorStatus) == 0 && (descriptorStatus.st_mode & TypeMask) == RegularType); + Check(sceKernelGetdents(regular, first.data(), static_cast(first.size())) == SCE_KERNEL_ERROR_EINVAL); + Check(sceKernelGetdents(regular, first.data(), 1) == SCE_KERNEL_ERROR_EINVAL); + void* invalidStream = fdopendir_nid_postfix(regular); + Check(invalidStream == nullptr); + Check(sceKernelClose(regular) == 0); + + const int replaced = sceKernelOpen("/app0/Data", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + const int replacement = sceKernelOpen("/app0/eboot.bin", SCE_KERNEL_O_RDONLY, 0); + Check(replaced >= 0 && replacement >= 0 && dup2_nid_postfix(replacement, replaced) == replaced); + Check(!File::IsDirectoryDescriptor(replaced) && sceKernelFstat(replaced, &descriptorStatus) == 0 && (descriptorStatus.st_mode & TypeMask) == RegularType); + Check(sceKernelClose(replaced) == 0 && sceKernelClose(replacement) == 0); + + const int duplicateFrom = sceKernelOpen("/app0/Data", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + const int duplicateTo = fcntl_nid_postfix(duplicateFrom, DuplicateFrom, duplicateFrom + 5); + Check(duplicateFrom >= 0 && duplicateTo >= duplicateFrom + 5 && File::IsDirectoryDescriptor(duplicateTo)); + Check(sceKernelClose(duplicateFrom) == 0 && File::IsDirectoryDescriptor(duplicateTo)); + Check(sceKernelClose(duplicateTo) == 0); + + const int owned = sceKernelOpen("/app0/Data", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Check(owned >= 0 && sceKernelGetdents(owned, first.data(), static_cast(first.size())) == 12); + void* stream = fdopendir_nid_postfix(owned); + Check(stream != nullptr); + auto* next = readdir_nid_postfix(stream); + Check(next != nullptr && std::strcmp(next->name, "..") == 0); + const auto cookie = telldir_nid_postfix(stream); + Check(cookie == 2); + next = readdir_nid_postfix(stream); + Check(next != nullptr && std::strcmp(next->name, "Big.bin") == 0); + seekdir_nid_postfix(stream, cookie); + next = readdir_nid_postfix(stream); + Check(next != nullptr && std::strcmp(next->name, "Big.bin") == 0); + Check(closedir_nid_postfix(stream) == 0); + Check(!DescriptorIsOpen_nid_no_patch(owned)); } void Reads() { @@ -155,10 +402,11 @@ void Refusals() { Check(sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL, 0666) == SCE_KERNEL_ERROR_EEXIST); Check(sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0) == SCE_KERNEL_ERROR_ENOTDIR); Check(sceKernelOpen("/app0/Data/Missing.txt", SCE_KERNEL_O_RDONLY, 0) == SCE_KERNEL_ERROR_ENOENT); - Check(NotImplemented([] { (void)sceKernelOpen("/app0/Data", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); })); - Check(NotImplemented([] { (void)sceKernelOpen("/app0/sce_sys", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); })); - Check(NotImplemented([] { (void)opendir_nid_postfix("/app0/sce_sys"); })); - Check(NotImplemented([] { (void)opendir_nid_postfix("/app0/Data"); })); + const int directory = sceKernelOpen("/app0/Data", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Check(directory >= 0 && sceKernelClose(directory) == 0); + void* archivedDirectory = opendir_nid_postfix("/app0/sce_sys"); + Check(archivedDirectory != nullptr && closedir_nid_postfix(archivedDirectory) == 0); + Directories(); const int fd = sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_RDONLY, 0); Check(fd >= 0); Check(NotImplemented([fd] { (void)fdopen_nid_postfix(fd, "r"); })); @@ -189,6 +437,52 @@ void WithoutArchive() { Check(directory >= 0 && sceKernelClose(directory) == 0); } +void NativeDirectoryIdentity(const std::filesystem::path& root) { + const auto original = root / "ordinary-directory-original"; + const auto renamed = root / "ordinary-directory-renamed"; + std::filesystem::create_directory(original); + { std::ofstream file(original / "original-entry"); file << "old"; } + const int fd = File::OpenDirectoryDescriptor(original); + Check(fd >= 0 && File::IsDirectoryDescriptor(fd) && !File::IsVirtualDirectoryDescriptor(fd) && File::IsNativeDirectoryDescriptor(fd)); + FileStat before{}; + Check(sceKernelFstat(fd, &before) == 0); + std::filesystem::rename(original, renamed); + std::filesystem::create_directory(original); + { std::ofstream file(original / "replacement-entry"); file << "new"; } +#ifdef _WIN32 + auto expectedPath = renamed; + auto expectedNative = expectedPath.native(); + if (expectedNative.size() >= 2 && expectedNative[1] == L':') { + if (expectedNative[0] >= L'A' && expectedNative[0] <= L'Z') expectedNative[0] += L'a' - L'A'; + else if (expectedNative[0] >= L'a' && expectedNative[0] <= L'z') expectedNative[0] -= L'a' - L'A'; + expectedPath = expectedNative; + } +#else + const auto expectedPath = renamed; +#endif + FileStat after{}; + Check(sceKernelFstat(fd, &after) == 0 && after.st_ino == before.st_ino); + const auto path = File::DirectoryDescriptorPath(fd); + Check(path.has_value()); + std::error_code pathError; + Check(std::filesystem::equivalent(*path, expectedPath, pathError) && !pathError); + std::error_code replacementError; + Check(!std::filesystem::equivalent(*path, original, replacementError) && !replacementError); + std::array entries{}; + const int bytes = sceKernelGetdents(fd, entries.data(), static_cast(entries.size())); + Check(bytes > 0); + const std::string listing(entries.data(), static_cast(bytes)); + Check(listing.find("original-entry") != std::string::npos && listing.find("replacement-entry") == std::string::npos); + Check(sceKernelFchmod(fd, 0700) == 0); + Check(sceKernelFutimes(fd, nullptr) == 0); + Check(sceKernelFsync(fd) == 0); + KernelStatfs fileSystem{}; + Check(_fstatfs_nid_postfix(fd, &fileSystem) == 0); + Check(sceKernelClose(fd) == 0); + std::filesystem::remove_all(original); + std::filesystem::remove_all(renamed); +} + } int main(int argc, char** argv) { @@ -200,6 +494,8 @@ int main(int argc, char** argv) { const auto root = AppArchiveFixture::EnterFreshRoot("anyps5-kernel-app-archive"); if (mode == "archive") { Archive(root); + FileNumbers(); + ConcurrentArchiveDirectoryEntries(); Reads(); Status(); Refusals(); @@ -213,6 +509,7 @@ int main(int argc, char** argv) { return 1; } else if (mode == "plain") { WithoutArchive(); + NativeDirectoryIdentity(root); } else { return 2; } diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index 34289a629a..0cc54fbfe9 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -2,6 +2,7 @@ #include "prx/libkernel/include/Api.hpp" #include "prx/libkernel/Semaphore/include/Semaphore.hpp" #include "prx/libkernel/Pthread/include/Mutex.hpp" +#include "prx/libkernel/Pthread/include/Rwlock.hpp" #include "prx/libSceSaveData/SaveDataCore.hpp" #include "SceTypes.hpp" #include @@ -26,6 +27,7 @@ static constexpr std::uint32_t EVF_WAITMODE_OR = 0x02; static constexpr std::uint32_t EVF_WAITMODE_CLEAR_ALL = 0x10; static constexpr std::uint32_t EVF_WAITMODE_CLEAR_PAT = 0x20; static constexpr int MUTEX_TYPE_ERRORCHECK = 1; +static constexpr int MUTEX_TYPE_NORMAL = 3; static constexpr int PRIO_NONE = 0; static constexpr int PRIO_INHERIT = 1; static constexpr int PRIO_PROTECT = 2; @@ -42,6 +44,13 @@ static void WaitForEventFlagWaiters(KernelEventFlag handle, int expected) { } } +template +static void RequireTimesOut(TLock lock) { + const auto start = std::chrono::steady_clock::now(); + Require(lock(20000) == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); +} + int main() { constexpr int maximum = std::numeric_limits::max(); KernelSema semaphore = nullptr; @@ -90,6 +99,7 @@ int main() { Require(scePthreadMutexattrDestroy(&attr) == SCE_OK); Require(scePthreadMutexLock(&mutex) == SCE_OK); Require(scePthreadMutexLock(&mutex) == SCE_KERNEL_ERROR_EDEADLK); + RequireTimesOut([&](KernelUseconds usec) { return scePthreadMutexTimedlock(&mutex, usec); }); Require(scePthreadMutexDestroy(&mutex) == SCE_KERNEL_ERROR_EBUSY); Require(scePthreadMutexUnlock(&mutex) == SCE_OK); Require(scePthreadMutexDestroy(&mutex) == SCE_OK); @@ -101,9 +111,41 @@ int main() { Require(scePthreadMutexattrDestroy(&attr) == SCE_OK); Require(scePthreadMutexLock(&adaptive) == SCE_OK); Require(scePthreadMutexLock(&adaptive) == SCE_KERNEL_ERROR_EDEADLK); + RequireTimesOut([&](KernelUseconds usec) { return scePthreadMutexTimedlock(&adaptive, usec); }); Require(scePthreadMutexUnlock(&adaptive) == SCE_OK); Require(scePthreadMutexDestroy(&adaptive) == SCE_OK); + PthreadMutex timedDefault = nullptr; + Require(scePthreadMutexInit(&timedDefault, nullptr, nullptr) == SCE_OK); + Require(scePthreadMutexLock(&timedDefault) == SCE_OK); + RequireTimesOut([&](KernelUseconds usec) { return scePthreadMutexTimedlock(&timedDefault, usec); }); + Require(scePthreadMutexUnlock(&timedDefault) == SCE_OK); + Require(scePthreadMutexTimedlock(&timedDefault, 20000) == SCE_OK); + Require(scePthreadMutexUnlock(&timedDefault) == SCE_OK); + Require(scePthreadMutexDestroy(&timedDefault) == SCE_OK); + + Require(scePthreadMutexattrInit(&attr) == SCE_OK); + Require(scePthreadMutexattrSettype(&attr, MUTEX_TYPE_NORMAL) == SCE_OK); + PthreadMutex normal = nullptr; + Require(scePthreadMutexInit(&normal, &attr, nullptr) == SCE_OK); + Require(scePthreadMutexattrDestroy(&attr) == SCE_OK); + Require(scePthreadMutexLock(&normal) == SCE_OK); + RequireTimesOut([&](KernelUseconds usec) { return scePthreadMutexTimedlock(&normal, usec); }); + Require(scePthreadMutexUnlock(&normal) == SCE_OK); + Require(scePthreadMutexDestroy(&normal) == SCE_OK); + + PthreadRwlock rwlock = nullptr; + Require(scePthreadRwlockInit(&rwlock, nullptr, nullptr) == SCE_OK); + Require(scePthreadRwlockWrlock(&rwlock) == SCE_OK); + Require(scePthreadRwlockTrywrlock(&rwlock) == SCE_KERNEL_ERROR_EBUSY); + Require(scePthreadRwlockWrlock(&rwlock) == SCE_KERNEL_ERROR_EDEADLK); + RequireTimesOut([&](KernelUseconds usec) { return scePthreadRwlockTimedrdlock(&rwlock, usec); }); + RequireTimesOut([&](KernelUseconds usec) { return scePthreadRwlockTimedwrlock(&rwlock, usec); }); + Require(scePthreadRwlockUnlock(&rwlock) == SCE_OK); + Require(scePthreadRwlockTimedwrlock(&rwlock, 20000) == SCE_OK); + Require(scePthreadRwlockUnlock(&rwlock) == SCE_OK); + Require(scePthreadRwlockDestroy(&rwlock) == SCE_OK); + PthreadMutex adaptiveStatic = reinterpret_cast(std::uintptr_t{1}); Require(scePthreadMutexLock(&adaptiveStatic) == SCE_OK); Require(adaptiveStatic != reinterpret_cast(std::uintptr_t{1})); diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 1e57a5b542..492633063c 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -283,7 +283,7 @@ static void FileFlagsRoundTrip() { char written[16]{}; Require(sceKernelRead(reading, written, sizeof(written)) == 8 && std::memcmp(written, "Ybcdeabc", 8) == 0); Require(sceKernelClose(reading) == 0); - Require(ThrowsWith([appending] { fcntl_nid_postfix(appending, SetFlags, Append | 0x80); }, "fcntl: changing O_FSYNC on a file is not implemented")); + Require(fcntl_nid_postfix(appending, SetFlags, Append | 0x80) == 0 && fcntl_nid_postfix(appending, GetFlags) == (WriteOnly | Append | 0x80)); Require(sceKernelClose(appending) == 0); std::remove("kernel_fcntl_probe.bin"); } diff --git a/core/libs/tests/GuestKernelFcntlSync.cpp b/core/libs/tests/GuestKernelFcntlSync.cpp new file mode 100644 index 0000000000..e5f6665f44 --- /dev/null +++ b/core/libs/tests/GuestKernelFcntlSync.cpp @@ -0,0 +1,96 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" std::uint64_t FileSynchronousWritesForTest_nid_no_patch(); + +namespace { + +constexpr int GetFlags = 3; +constexpr int SetFlags = 4; +constexpr int Synchronous = 0x80; +constexpr int ReadWrite = 2; +constexpr int Create = 0x200; + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "check failed at line %u\n", static_cast(location.line())); + std::exit(1); +} + +std::uint64_t Synced() { + return FileSynchronousWritesForTest_nid_no_patch(); +} + +void Toggle(const std::string& path) { + const int file = open_nid_postfix(path.c_str(), ReadWrite | Create, 0600); + Require(file >= 0); + Require((fcntl_nid_postfix(file, GetFlags) & Synchronous) == 0); + auto before = Synced(); + Require(write_nid_postfix(file, "a", 1) == 1 && Synced() == before); + + Require(fcntl_nid_postfix(file, SetFlags, Synchronous) == 0); + Require((fcntl_nid_postfix(file, GetFlags) & Synchronous) != 0); + before = Synced(); + Require(write_nid_postfix(file, "b", 1) == 1 && Synced() == before + 1); + char c[] = "c", d[] = "d", y[] = "y", z[] = "z"; + const KernelIovec vectors[] = {{c, 1}, {d, 1}}; + Require(writev_nid_postfix(file, vectors, 2) == 2 && Synced() == before + 2); + Require(pwrite_nid_disambig1_nid_postfix(file, "x", 1, 10) == 1 && Synced() == before + 3); + const KernelIovec tail[] = {{y, 1}, {z, 1}}; + Require(pwritev_nid_postfix(file, tail, 2, 11) == 2 && Synced() == before + 4); + + const int duplicate = dup_nid_postfix(file); + Require(duplicate >= 0 && (fcntl_nid_postfix(duplicate, GetFlags) & Synchronous) != 0); + Require(write_nid_postfix(duplicate, "e", 1) == 1 && Synced() == before + 5); + + Require(fcntl_nid_postfix(duplicate, SetFlags, 0) == 0); + Require((fcntl_nid_postfix(file, GetFlags) & Synchronous) == 0); + before = Synced(); + Require(write_nid_postfix(file, "f", 1) == 1 && Synced() == before); + + char contents[13] = {}; + Require(pread_nid_postfix(file, contents, sizeof(contents), 0) == 13); + Require(std::memcmp(contents, "abcdef", 6) == 0 && std::memcmp(contents + 10, "xyz", 3) == 0); + Require(close_nid_postfix(duplicate) == 0 && close_nid_postfix(file) == 0); +} + +void OpenedSynchronous(const std::string& path) { + const int file = open_nid_postfix(path.c_str(), ReadWrite | Create | Synchronous, 0600); + Require(file >= 0 && (fcntl_nid_postfix(file, GetFlags) & Synchronous) != 0); + const auto before = Synced(); + Require(write_nid_postfix(file, "h", 1) == 1 && Synced() == before + 1); + Require(close_nid_postfix(file) == 0); +} + +void Pipes() { + int ends[2] = {-1, -1}; + Require(pipe_nid_postfix(ends) == 0); + Require(fcntl_nid_postfix(ends[1], SetFlags, Synchronous) == 0); + Require((fcntl_nid_postfix(ends[1], GetFlags) & Synchronous) != 0); + const auto before = Synced(); + Require(write_nid_postfix(ends[1], "i", 1) == 1 && Synced() == before); + Require(close_nid_postfix(ends[0]) == 0 && close_nid_postfix(ends[1]) == 0); +} + +} + +int main() { + const auto stamp = std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const std::string first = "anyps5-fsync-" + stamp + ".bin"; + const std::string second = "anyps5-fsync-open-" + stamp + ".bin"; + Toggle(first); + OpenedSynchronous(second); + Pipes(); + std::filesystem::remove(first); + std::filesystem::remove(second); + std::puts("guest_kernel_fcntl_sync passed"); + return 0; +} diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index a3c4eeebf1..126dce19e7 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -1,10 +1,12 @@ #include "prx/libkernel_sys/include/Api.hpp" #include "prx/libkernel/include/Api.hpp" #include "prx/libkernel/Pthread/include/Cond.hpp" +#include "prx/libkernel/Time/include/Time.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libc/include/Api.hpp" #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/General.hpp" #include "prx/libc/include/RemoveTree.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" #include "prx/libkernel/KernelErrors.hpp" @@ -30,6 +32,7 @@ #endif #ifndef _WIN32 #include +#include #include #endif @@ -44,16 +47,6 @@ int Error() { return *__error_nid_postfix(); } -bool PathconfThrows(const char* path, int name) { - try { - pathconf_nid_postfix(path, name); - - } catch (const std::runtime_error&) { - return true; - } - return false; -} - bool ChrootThrows(const std::string& path) { try { chroot_nid_postfix(path.c_str()); @@ -115,8 +108,26 @@ void OwnershipAndLinkTests(const std::filesystem::path& root) { Require(linkat_nid_postfix(-100, file.c_str(), -100, other.c_str(), 0x200) == -1 && Error() == 22); Require(linkat_nid_postfix(-100, nullptr, -100, other.c_str(), 0) == -1 && Error() == 14); Require(linkat_nid_postfix(-100, "", -100, other.c_str(), 0) == -1 && Error() == 2); - Require(ThrowsNaming([&] { linkat_nid_postfix(directory, "owned.txt", -100, other.c_str(), 0); }, "linkat: a relative path")); - Require(ThrowsNaming([&] { linkat_nid_postfix(-100, file.c_str(), directory, "other.txt", 0); }, "linkat: a relative path")); + Require(linkat_nid_postfix(directory, "owned.txt", -100, other.c_str(), 0) == 0); + Require(std::filesystem::hard_link_count(file) == 3 && std::filesystem::remove(other)); + Require(linkat_nid_postfix(-100, file.c_str(), directory, "other.txt", 0) == 0); + Require(std::filesystem::hard_link_count(file) == 3); + Require(linkat_nid_postfix(directory, "owned.txt", directory, "other.txt", 0) == -1 && Error() == 17); + Require(linkat_nid_postfix(directory, "missing.txt", directory, "absent.txt", 0) == -1 && Error() == 2); + Require(linkat_nid_postfix(4321, "owned.txt", directory, "absent.txt", 0) == -1 && Error() == 9); + Require(linkat_nid_postfix(directory, "owned.txt", 4321, "absent.txt", 0) == -1 && Error() == 9); + Require(linkat_nid_postfix(directory, "missing.txt", 4321, "absent.txt", 0) == -1 && Error() == 2); + Require(linkat_nid_postfix(directory, "missing.txt", -100, nullptr, 0) == -1 && Error() == 2); + Require(linkat_nid_postfix(-100, root.string().c_str(), 4321, "absent.txt", 0) == -1 && Error() == 1); + Require(linkat_nid_postfix(-100, root.string().c_str(), -100, "", 0) == -1 && Error() == 1); + Require(linkat_nid_postfix(directory, "owned.txt", -100, nullptr, 0) == -1 && Error() == 14); + Require(linkat_nid_postfix(directory, "owned.txt", -100, "", 0) == -1 && Error() == 2); + const int plain = sceKernelOpen(file.c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(plain >= 0); + Require(linkat_nid_postfix(plain, "owned.txt", directory, "absent.txt", 0) == -1 && Error() == 20); + Require(linkat_nid_postfix(directory, "owned.txt", plain, "absent.txt", 0) == -1 && Error() == 20); + Require(sceKernelClose(plain) == 0 && std::filesystem::remove(other)); + Require(!std::filesystem::exists(root / "absent.txt")); const auto symbolic = (root / "owned.link").string(); const auto dangling = (root / "dangling.link").string(); if (symlink_nid_postfix("owned.txt", symbolic.c_str()) == 0) { @@ -163,6 +174,119 @@ bool RemoveSymlink(const std::string& link) { #endif } +class GuardedPaths { +public: + GuardedPaths() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + page = info.dwPageSize; + memory = static_cast(VirtualAlloc(nullptr, 2 * page, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(memory != nullptr); + DWORD previous = 0; + Require(VirtualProtect(memory + page, page, PAGE_NOACCESS, &previous) != 0); +#else + const long size = sysconf(_SC_PAGESIZE); + Require(size > 0); + page = static_cast(size); + memory = static_cast(mmap(nullptr, 2 * page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(memory != MAP_FAILED); + Require(mprotect(memory + page, page, PROT_NONE) == 0); +#endif + } + + ~GuardedPaths() { +#ifdef _WIN32 + Require(VirtualFree(memory, 0, MEM_RELEASE) != 0); +#else + Require(munmap(memory, 2 * page) == 0); +#endif + } + + char* Unreadable() const { return memory + page; } + char* Crossing() const { return memory + page - 3; } + char* Boundary() const { return memory + page - 2; } + +private: + char* memory = nullptr; + std::size_t page = 0; +}; + +void CheckUnreadablePaths(const std::string& renameSource, int directory) { + constexpr int Fault = 14; + constexpr int KernelFault = static_cast(0x8002000Eu); + GuardedPaths paths; + std::memcpy(paths.Crossing(), "abc", 3); + std::string copy; + Require(CopyGuestPath_nid_no_patch(paths.Unreadable(), copy) == Fault); + Require(CopyGuestPath_nid_no_patch(paths.Crossing(), copy) == Fault); + paths.Boundary()[0] = '.'; + paths.Boundary()[1] = '\0'; + Require(CopyGuestPath_nid_no_patch(paths.Boundary(), copy) == 0 && copy == "."); + const std::string maximum(1023, 'p'); + std::array unterminated{}; + unterminated.fill('p'); + Require(CopyGuestPath_nid_no_patch(maximum.c_str(), copy) == 0 && copy == maximum); + Require(CopyGuestPath_nid_no_patch(unterminated.data(), copy) == 63); + const auto posixFault = [&](auto call) { + *__error_nid_postfix() = 0; + Require(call() == -1 && Error() == Fault); + }; + FileStat status{}; + char linkBuffer[16]{}; + posixFault([&] { return open_nid_postfix(paths.Unreadable(), SCE_KERNEL_O_RDONLY, 0); }); + posixFault([&] { return chmod_nid_postfix(paths.Unreadable(), 0); }); + posixFault([&] { return mkdir_nid_postfix(paths.Unreadable(), 0755); }); + posixFault([&] { return rmdir_nid_postfix(paths.Unreadable()); }); + posixFault([&] { return unlink_nid_postfix(paths.Unreadable()); }); + posixFault([&] { return stat_nid_postfix(paths.Unreadable(), &status); }); + posixFault([&] { return lstat_nid_postfix(paths.Unreadable(), &status); }); + posixFault([&] { return truncate_nid_postfix(paths.Unreadable(), 0); }); + posixFault([&] { return utimes_nid_postfix(paths.Unreadable(), nullptr); }); + posixFault([&] { return lutimes_nid_postfix(paths.Unreadable(), nullptr); }); + posixFault([&] { return readlink_nid_postfix(paths.Unreadable(), linkBuffer, sizeof(linkBuffer)); }); + posixFault([&] { return pathconf_nid_postfix(paths.Unreadable(), 4); }); + posixFault([&] { return link_nid_postfix(paths.Unreadable(), "unused-link"); }); + posixFault([&] { return link_nid_postfix(renameSource.c_str(), paths.Unreadable()); }); + posixFault([&] { return symlink_nid_postfix(paths.Unreadable(), "unused-symlink"); }); + posixFault([&] { return symlink_nid_postfix("target", paths.Unreadable()); }); + posixFault([&] { return openat_nid_postfix(-100, paths.Unreadable(), SCE_KERNEL_O_RDONLY); }); + posixFault([&] { return mkdirat_nid_postfix(-100, paths.Unreadable(), 0755); }); + posixFault([&] { return fchmodat_nid_postfix(-100, paths.Unreadable(), 0, 0); }); + posixFault([&] { return lchmod_nid_postfix(paths.Unreadable(), 0); }); + posixFault([&] { return fstatat_nid_postfix(-100, paths.Unreadable(), &status, 0); }); + posixFault([&] { return unlinkat_nid_postfix(-100, paths.Unreadable(), 0); }); + posixFault([&] { return readlinkat_nid_postfix(-100, paths.Unreadable(), linkBuffer, sizeof(linkBuffer)); }); + posixFault([&] { return renameat_nid_postfix(-100, paths.Unreadable(), -100, "unused-rename"); }); + posixFault([&] { return renameat_nid_postfix(-100, renameSource.c_str(), -100, paths.Unreadable()); }); + posixFault([&] { return openat_nid_postfix(directory, paths.Unreadable(), SCE_KERNEL_O_RDONLY); }); + posixFault([&] { return mkdirat_nid_postfix(directory, paths.Unreadable(), 0755); }); + posixFault([&] { return fchmodat_nid_postfix(directory, paths.Unreadable(), 0, 0); }); + posixFault([&] { return fstatat_nid_postfix(directory, paths.Unreadable(), &status, 0); }); + posixFault([&] { return unlinkat_nid_postfix(directory, paths.Unreadable(), 0); }); + posixFault([&] { return readlinkat_nid_postfix(directory, paths.Unreadable(), linkBuffer, sizeof(linkBuffer)); }); + posixFault([&] { return renameat_nid_postfix(directory, paths.Unreadable(), directory, "unused-rename"); }); + posixFault([&] { return renameat_nid_postfix(directory, "file.txt", directory, paths.Unreadable()); }); + *__error_nid_postfix() = 0; + Require(renameat_nid_postfix(directory, "missing", directory, paths.Unreadable()) == -1 && Error() == 2); + *__error_nid_postfix() = 0; + Require(link_nid_postfix("missing", paths.Unreadable()) == -1 && Error() == 2); + posixFault([&] { return shm_open_nid_postfix(paths.Unreadable(), SCE_KERNEL_O_RDWR, 0600); }); + posixFault([&] { return shm_unlink_nid_postfix(paths.Unreadable()); }); + Require(sceKernelOpen(paths.Unreadable(), SCE_KERNEL_O_RDONLY, 0) == KernelFault); + Require(sceKernelStat(paths.Unreadable(), &status) == KernelFault); + Require(sceKernelMkdir(paths.Unreadable(), 0755) == KernelFault); + Require(sceKernelRmdir(paths.Unreadable()) == KernelFault); + Require(sceKernelUnlink(paths.Unreadable()) == KernelFault); + Require(sceKernelChmod_nid_postfix(paths.Unreadable(), 0) == KernelFault); + Require(sceKernelTruncate_nid_postfix(paths.Unreadable(), 0) == KernelFault); + Require(sceKernelUtimes_nid_postfix(paths.Unreadable(), nullptr) == KernelFault); + Require(sceKernelCheckReachability(paths.Unreadable()) == KernelFault); + Require(sceKernelRename(paths.Unreadable(), ".") == KernelFault); + Require(sceKernelRename(renameSource.c_str(), paths.Unreadable()) == KernelFault); + Require(!std::filesystem::exists("unused-link") && !std::filesystem::exists("unused-symlink") && !std::filesystem::exists("unused-rename")); +} + void CheckSymlinkTargetRestrictions(const std::filesystem::path& root) { const auto created = (root / "rejected").string(); for (const auto& target : {std::string(".."), std::string("../x"), std::string("a/../b"), std::string("a/../../x"), std::string("/abs")}) @@ -227,11 +351,13 @@ void CheckPathconf(const std::filesystem::path& root, const std::string& file) { Require(pathconf_nid_postfix(path.c_str(), 18) == 1024); Require(pathconf_nid_postfix(path.c_str(), 54) == 0); Require(pathconf_nid_postfix(path.c_str(), 55) == 0); + for (const int name : {10, 14, 16, 21}) Require(pathconf_nid_postfix(path.c_str(), name) == 65536); + Require(pathconf_nid_postfix(path.c_str(), 17) == 16384); + Require(pathconf_nid_postfix(path.c_str(), 60) == 3); + for (const int name : {59, 63, 64}) Require(pathconf_nid_postfix(path.c_str(), name) == 0); } - for (const int name : {0, 11, 61, 62, 65, -1}) + for (const int name : {0, 2, 3, 9, 11, 53, 61, 62, 65, -1}) Require(pathconf_nid_postfix(file.c_str(), name) == -1 && Error() == 22); - for (const int name : {2, 3, 9, 10, 14, 16, 17, 21, 53, 59, 60, 63, 64}) - Require(PathconfThrows(file.c_str(), name)); const auto missing = (root / "missing").string(); Require(pathconf_nid_postfix(missing.c_str(), 4) == -1 && Error() == 2); Require(pathconf_nid_postfix(missing.c_str(), 0) == -1 && Error() == 2); @@ -245,6 +371,31 @@ void CheckPathconf(const std::filesystem::path& root, const std::string& file) { Require(pathconf_nid_postfix(longPath.c_str(), 4) == -1 && Error() == 63); } +void CheckSettimeofday() { + KernelTimezone zone{-1, -1}; + Require(settimeofday_nid_postfix(nullptr, nullptr) == 0); + Require(gettimeofday_nid_postfix(nullptr, &zone) == 0 && zone.tz_minuteswest == 0 && zone.tz_dsttime == 0); + const KernelTimezone west{60, 1}; + for (const KernelTimeval invalid : {KernelTimeval{0, 1000000}, KernelTimeval{0, -1}, KernelTimeval{-1, 0}}) + Require(settimeofday_nid_postfix(&invalid, &west) == -1 && Error() == 22); + Require(gettimeofday_nid_postfix(nullptr, &zone) == 0 && zone.tz_minuteswest == 0 && zone.tz_dsttime == 0); + const KernelTimeval valid{1000000000, 0}; + bool rejected = false; + try { + settimeofday_nid_postfix(&valid, &west); + } catch (const std::runtime_error& error) { + rejected = std::strstr(error.what(), "settimeofday: setting the time to 1000000000.000000") != nullptr; + } + Require(rejected); + Require(gettimeofday_nid_postfix(nullptr, &zone) == 0 && zone.tz_minuteswest == 0 && zone.tz_dsttime == 0); + Require(settimeofday_nid_postfix(nullptr, &west) == 0); + KernelTimeval now{}; + Require(gettimeofday_nid_postfix(&now, &zone) == 0 && zone.tz_minuteswest == 60 && zone.tz_dsttime == 1 && now.tv_sec > 0); + const KernelTimezone zero{0, 0}; + Require(settimeofday_nid_postfix(nullptr, &zero) == 0); + Require(gettimeofday_nid_postfix(nullptr, &zone) == 0 && zone.tz_minuteswest == 0 && zone.tz_dsttime == 0); +} + void DirectoryRenameTests(const std::filesystem::path& parent) { const auto fixture = parent / "directory-identity"; const auto original = fixture / "original"; @@ -343,16 +494,6 @@ static void CheckProcessQueries() { Require(sceKernelGetProcessType(getpid_nid_postfix() + 1) == static_cast(0x80020003)); } -template -bool DescriptorPathconfThrows(F fn, int descriptor, int name) { - try { - fn(descriptor, name); - } catch (const std::runtime_error&) { - return true; - } - return false; -} - template void CheckDescriptorPathconf(F fn, int descriptor) { Require(fn(descriptor, 1) == 32767); @@ -367,11 +508,13 @@ void CheckDescriptorPathconf(F fn, int descriptor) { Require(fn(descriptor, 18) == 1024); Require(fn(descriptor, 54) == 0); Require(fn(descriptor, 55) == 0); - for (const int name : {0, 11, 61, 62, 65, -1}) + for (const int name : {10, 14, 16, 21}) Require(fn(descriptor, name) == 65536); + Require(fn(descriptor, 17) == 16384); + Require(fn(descriptor, 60) == 3); + for (const int name : {59, 63, 64}) Require(fn(descriptor, name) == 0); + for (const int name : {0, 2, 3, 9, 11, 61, 62, 65, -1}) Require(fn(descriptor, name) == -1 && Error() == 22); Require(fn(descriptor, 53) == 200112); - for (const int name : {2, 3, 9, 10, 14, 16, 17, 21, 59, 60, 63, 64}) - Require(DescriptorPathconfThrows(fn, descriptor, name)); Require(fn(-1, 4) == -1 && Error() == 9); Require(fn(descriptor + 1000, 4) == -1 && Error() == 9); } @@ -533,7 +676,13 @@ int main() { #endif std::error_code error; const auto link = (root / "link").string(); +#ifdef _WIN32 + if (!CreateSymbolicLinkW(std::filesystem::path(link).c_str(), L"sub\\target.txt", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE)) { + error = std::error_code(GetLastError(), std::system_category()); + } +#else std::filesystem::create_symlink("sub/target.txt", link, error); +#endif if (!error) { std::memset(buffer, '#', sizeof(buffer)); Require(readlink_nid_postfix(link.c_str(), buffer, sizeof(buffer)) == 14); @@ -578,6 +727,7 @@ int main() { Require(link_nid_postfix(nullptr, linked.c_str()) == -1 && Error() == 14); Require(link_nid_postfix(original.c_str(), "") == -1 && Error() == 2); Require(std::filesystem::remove(linked) && std::filesystem::hard_link_count(original) == 1); + CheckSettimeofday(); const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); Require(directory >= 0); const int fileDescriptor = sceKernelOpen(file.c_str(), SCE_KERNEL_O_RDONLY, 0); @@ -719,6 +869,7 @@ int main() { CheckSymlinkTargetRestrictions(root); const int base = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); Require(base >= 0); + CheckUnreadablePaths(file, base); const int relative = openat_nid_postfix(base, "file.txt", SCE_KERNEL_O_RDONLY); char byte = 0; Require(relative >= 0 && sceKernelRead(relative, &byte, 1) == 1 && byte == 'x'); @@ -940,7 +1091,12 @@ int main() { #endif Require(fchown_nid_postfix(base, 0, 0) == 0); Require(fchown_nid_postfix(base, 0xFFFFFFFFu, 0xFFFFFFFFu) == 0); + Require(fchown_nid_postfix(base, 0, 0xFFFFFFFFu) == 0); + Require(fchown_nid_postfix(base, 0xFFFFFFFFu, 0) == 0); Require(fchown_nid_postfix(-1, 0, 0) == -1 && Error() == 9); + Require(fchown_nid_postfix(-1, 1, 0) == -1 && Error() == 9); + Require(fchown_nid_postfix(-1, 0, 1) == -1 && Error() == 9); + Require(fchown_nid_postfix(-1, 1, 1) == -1 && Error() == 9); bool fchownThrew = false; try { fchown_nid_postfix(base, 1, 0); } catch (const std::exception&) { fchownThrew = true; } Require(fchownThrew); diff --git a/core/libs/tests/GuestKernelSysConsoleIdentity.cpp b/core/libs/tests/GuestKernelSysConsoleIdentity.cpp new file mode 100644 index 0000000000..f01ac5f625 --- /dev/null +++ b/core/libs/tests/GuestKernelSysConsoleIdentity.cpp @@ -0,0 +1,71 @@ +#include "prx/libkernel_sys/libkernel_sys.hpp" +#include "prx/libc/libc.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr int GuestEperm = 1; +constexpr int GuestEsrch = 3; + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "KERNEL SYS CONSOLE IDENTITY: %s\n", message); + std::abort(); + } +} + +void CheckConsoleKind() { + Require(sceKernelIsCEX() == 1, "the console must be reported as a retail unit"); + Require(sceKernelIsDevKit() == 0, "the console must not be reported as a development kit"); +} + +void CheckAuthInfo() { + struct { + KernelAuthInfo value; + std::uint64_t guard; + } info; + *__error_nid_postfix() = 0; + Require(get_authinfo_nid_postfix(0, nullptr) == -1 && *__error_nid_postfix() == GuestEperm, "a null result must fail with EPERM"); + std::memset(&info, 0x5a, sizeof(info)); + *__error_nid_postfix() = 0; + Require(get_authinfo_nid_postfix(getpid_nid_postfix() + 1, &info.value) == -1 && *__error_nid_postfix() == GuestEsrch, + "another process must fail with ESRCH"); + for (const auto byte : std::bit_cast>(info)) + Require(byte == 0x5a, "a refused call must not write the result"); + for (const int pid : {0, getpid_nid_postfix()}) { + std::memset(&info, 0x5a, sizeof(info)); + Require(get_authinfo_nid_postfix(pid, &info.value) == 0, "the guest process must be described"); + Require(info.value.programAuthorityId == 0 && info.value.capabilities[0] == 0x2000000000000000, "the authority ID or first capability word is wrong"); + const auto bytes = std::bit_cast>(info.value); + for (std::size_t i = 16; i < bytes.size(); ++i) Require(bytes[i] == 0, "the rest of the result must be cleared"); + Require(info.guard == 0x5a5a5a5a5a5a5a5a, "get_authinfo wrote past its 0x88-byte result"); + } +} + +void CheckOpenPsId() { + std::array plain{}; + std::array system; + system.fill(0xa5); + *__error_nid_postfix() = GuestEsrch; + Require(sceKernelGetOpenPsIdForSystem(nullptr) == 22, "a null system OpenPSID must return positive EINVAL"); + Require(*__error_nid_postfix() == GuestEsrch, "a null system OpenPSID must leave errno unchanged"); + for (auto byte : system) Require(byte == 0xa5, "a null system OpenPSID must not write output"); + Require(sceKernelGetOpenPsId(plain.data()) == 0, "the OpenPSID must be readable"); + Require(sceKernelGetOpenPsIdForSystem(system.data()) == 0, "the system OpenPSID must be readable"); + Require(std::memcmp(plain.data(), system.data(), plain.size()) == 0, "both OpenPSID queries must agree"); + for (std::size_t i = plain.size(); i < system.size(); ++i) Require(system[i] == 0xa5, "the system OpenPSID wrote past 16 bytes"); +} + +} + +int main() { + CheckConsoleKind(); + CheckAuthInfo(); + CheckOpenPsId(); + return 0; +} diff --git a/core/libs/tests/GuestKernelSysConstants.cpp b/core/libs/tests/GuestKernelSysConstants.cpp new file mode 100644 index 0000000000..8565415fc8 --- /dev/null +++ b/core/libs/tests/GuestKernelSysConstants.cpp @@ -0,0 +1,45 @@ +#include "SceTypes.hpp" +#include "prx/libkernel_sys/libkernel_sys.hpp" +#include + +namespace { + +constexpr int GUEST_ENOTSUP = 45; +constexpr int SCE_KERNEL_ERROR_EPERM = static_cast(0x80020001); +constexpr int SCE_KERNEL_ERROR_ENOENT = static_cast(0x80020002); +constexpr int SCE_KERNEL_ERROR_ENOTSUP = static_cast(0x8002002d); +constexpr int SCE_KERNEL_ERROR_ENOSYS = static_cast(0x8002004e); + +void Require(bool value) { if (!value) std::abort(); } + +} + +int main() { + Require(pthread_switch_add_np_nid_postfix() == GUEST_ENOTSUP); + Require(pthread_switch_delete_np_nid_postfix() == GUEST_ENOTSUP); + + Require(sceKernelAddResource() == 0); + Require(sceKernelEventLogInit() == 0); + Require(sceKernelIsGenuineKratosCex() == 0); + Require(sceKernelReleaseTraceDirectMemory() == 0); + + Require(sceKernelHasNeoMode() == 1); + Require(sceKernelIsAuthenticNeo() == 1); + Require(sceKernelIsGenuineN() == 1); + + Require(sceKernelCreateEport() == SCE_KERNEL_ERROR_EPERM); + Require(sceKernelDeleteEport() == SCE_KERNEL_ERROR_EPERM); + Require(sceKernelOpenEport() == SCE_KERNEL_ERROR_EPERM); + Require(sceKernelCloseEport() == SCE_KERNEL_ERROR_EPERM); + Require(sceKernelTriggerEport() == SCE_KERNEL_ERROR_EPERM); + + Require(sceKernelQueryTraceMemory() == SCE_KERNEL_ERROR_ENOENT); + + Require(sceKernelIccGetCPMode() == SCE_KERNEL_ERROR_ENOTSUP); + Require(sceKernelIccSetCPMode() == SCE_KERNEL_ERROR_ENOTSUP); + + Require(sceKernelClearGameDirectMemory() == SCE_KERNEL_ERROR_ENOSYS); + Require(sceKernelMapTraceMemory() == SCE_KERNEL_ERROR_ENOSYS); + Require(sceKernelTraceMemoryTypeProtect() == SCE_KERNEL_ERROR_ENOSYS); + return 0; +} diff --git a/core/libs/tests/GuestKernelSysLogin.cpp b/core/libs/tests/GuestKernelSysLogin.cpp new file mode 100644 index 0000000000..44c5f217ed --- /dev/null +++ b/core/libs/tests/GuestKernelSysLogin.cpp @@ -0,0 +1,76 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +void LoginNameForTest_nid_no_patch(const char* name); +} + +namespace { + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "check failed at line %u\n", static_cast(location.line())); + std::exit(1); +} + +int Error() { + return *__error_nid_postfix(); +} + +void Getlogin() { + char expected[33]{}; + const int error = getlogin_r_nid_postfix(expected, static_cast(sizeof(expected))); + char* name = getlogin_nid_postfix(); + if (error != 0) { + Require(name == nullptr && Error() == error); + return; + } + if (expected[0] == '\0') { + Require(name == nullptr); + return; + } + Require(name != nullptr && std::strcmp(name, expected) == 0); + Require(getlogin_nid_postfix() == name); +} + +void GetloginPaths() { + LoginNameForTest_nid_no_patch("player"); + char* name = getlogin_nid_postfix(); + Require(name != nullptr && std::strcmp(name, "player") == 0); + char buffer[7]; + std::memset(buffer, 'x', sizeof(buffer)); + Require(getlogin_r_nid_postfix(buffer, 6) == 34 && buffer[0] == 'x'); + Require(getlogin_r_nid_postfix(buffer, 7) == 0 && std::strcmp(buffer, "player") == 0); + + const std::string longName(40, 'n'); + LoginNameForTest_nid_no_patch(longName.c_str()); + *__error_nid_postfix() = 0; + Require(getlogin_nid_postfix() == nullptr && Error() == 34); + + LoginNameForTest_nid_no_patch(""); + bool threw = false; + try { + getlogin_nid_postfix(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + LoginNameForTest_nid_no_patch(nullptr); +} + +} + +int main() { + Getlogin(); + GetloginPaths(); + std::puts("getlogin passed"); + return 0; +} diff --git a/core/libs/tests/GuestKernelSysModuleInternal.cpp b/core/libs/tests/GuestKernelSysModuleInternal.cpp index 2674a6ab3d..a51226a5e9 100644 --- a/core/libs/tests/GuestKernelSysModuleInternal.cpp +++ b/core/libs/tests/GuestKernelSysModuleInternal.cpp @@ -74,10 +74,22 @@ static void CheckRefusals() { Require(sceKernelGetModuleInfoInternal(-1, &unknown) == SCE_KERNEL_ERROR_ESRCH && unknown.name[0] == '\0'); } +static void CheckLoadStartSysmodule() { + int res = -1; + Require(sceKernelLoadStartModuleForSysmodule(nullptr, 0, nullptr, 0, nullptr, &res) == static_cast(SCE_KERNEL_ERROR_EFAULT)); + Require(res == 0); + Require(sceKernelLoadStartModuleForSysmodule(nullptr, 0, nullptr, 0, nullptr, nullptr) == static_cast(SCE_KERNEL_ERROR_EFAULT)); + res = -1; + Require(sceKernelLoadStartModuleForSysmodule("/system/common/lib/nonexistent_sysmodule.sprx", 0, nullptr, 0, nullptr, &res) == + static_cast(SCE_KERNEL_ERROR_ENOENT)); + Require(res == 0); +} + int main() { CheckList(); CheckEachModule(); CheckMatchesAddressLookup(); CheckRefusals(); + CheckLoadStartSysmodule(); return 0; } diff --git a/core/libs/tests/GuestKernelSysPselect.cpp b/core/libs/tests/GuestKernelSysPselect.cpp index 773ab2b80a..c4cb2f25a5 100644 --- a/core/libs/tests/GuestKernelSysPselect.cpp +++ b/core/libs/tests/GuestKernelSysPselect.cpp @@ -12,6 +12,7 @@ namespace { +constexpr int Eintr = 4; constexpr int Einval = 22; constexpr int SigBlock = 1; constexpr int SigSetmask = 3; @@ -52,6 +53,17 @@ bool Same(const SignalSet& left, const SignalSet& right) { return std::memcmp(left.bits, right.bits, sizeof(left.bits)) == 0; } +int caught = 0; + +void APS5_VABI Count(int) { + ++caught; +} + +void RaiseBlocked(const SignalSet& blocked) { + Require(sigprocmask_nid_postfix(SigBlock, &blocked, nullptr) == 0); + Require(raise_nid_postfix(SigUsr1) == 0 && caught == 0); +} + } int main() { @@ -94,6 +106,60 @@ int main() { const Timespec zero{0, 0}; Require(pselect_nid_postfix(-1, nullptr, nullptr, nullptr, &zero, nullptr) == -1 && Error() == Einval); } + Require(signal_nid_postfix(SigUsr1, Count) != reinterpret_cast(static_cast(-1))); + const SignalSet empty{}; + { + RaiseBlocked(blocked); + const SignalSet withUsr1 = CurrentMask(); + DescriptorSet read{}; + read.Add(writer); + const DescriptorSet original = read; + const Timespec limit{0, 300000000}; + const auto start = std::chrono::steady_clock::now(); + Require(pselect_nid_postfix(writer + 1, read.words.data(), nullptr, nullptr, &limit, &empty) == -1 && Error() == Eintr); + Require(std::chrono::steady_clock::now() - start < std::chrono::milliseconds(150)); + Require(caught == 1); + Require(read.words == original.words); + Require(Same(CurrentMask(), withUsr1)); + caught = 0; + } + { + RaiseBlocked(blocked); + Require(pselect_nid_postfix(0, nullptr, nullptr, nullptr, nullptr, &empty) == -1 && Error() == Eintr && caught == 1); + caught = 0; + } + { + RaiseBlocked(blocked); + DescriptorSet read{}; + read.Add(reader); + const Timespec limit{0, 300000000}; + Require(pselect_nid_postfix(reader + 1, read.words.data(), nullptr, nullptr, &limit, &empty) == 1 && caught == 1); + Require(read.Has(reader)); + caught = 0; + } + { + RaiseBlocked(blocked); + const Timespec zero{0, 0}; + DescriptorSet read{}; + read.Add(writer); + Require(pselect_nid_postfix(writer + 1, read.words.data(), nullptr, nullptr, &zero, &empty) == 0 && caught == 1); + caught = 0; + } + { + RaiseBlocked(blocked); + const Timespec invalid{0, 1000000000}; + Require(pselect_nid_postfix(0, nullptr, nullptr, nullptr, &invalid, &empty) == -1 && Error() == Einval && caught == 1); + caught = 0; + } + { + RaiseBlocked(blocked); + const Timespec limit{0, 200000000}; + const auto start = std::chrono::steady_clock::now(); + Require(pselect_nid_postfix(0, nullptr, nullptr, nullptr, &limit, &blocked) == 0 && caught == 0); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(150)); + Require(sigprocmask_nid_postfix(SigSetmask, &empty, nullptr) == 0 && caught == 1); + caught = 0; + } Require(sigprocmask_nid_postfix(SigSetmask, &before, nullptr) == 0); Require(close_nid_postfix(reader) == 0 && close_nid_postfix(writer) == 0); std::puts("pselect passed"); diff --git a/core/libs/tests/GuestKernelSysPthreadNp.cpp b/core/libs/tests/GuestKernelSysPthreadNp.cpp new file mode 100644 index 0000000000..a1cd8ca017 --- /dev/null +++ b/core/libs/tests/GuestKernelSysPthreadNp.cpp @@ -0,0 +1,111 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/Pthread/include/Rwlock.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + +namespace { + +constexpr int GUEST_EDEADLK = 11; +constexpr int GUEST_EINVAL = 22; +constexpr int SCE_FAILED_CALL = static_cast(0x8001ffffu); +constexpr unsigned SCE_KERNEL_ERROR_BASE = 0x80020000u; +constexpr int KIND_ERRORCHECK = 1; +constexpr int KIND_RECURSIVE = 2; +constexpr int KIND_NORMAL = 3; +constexpr int KIND_ADAPTIVE = 4; + +int failures = 0; + +template +void Case(const char* name, TCall call) { + try { + if (call()) return; + std::printf("%s: wrong answer\n", name); + } catch (const std::exception& error) { + std::printf("%s: %s\n", name, error.what()); + } + ++failures; +} + +template +bool Throws(TCall call) { + try { + static_cast(call()); + } catch (const TException&) { + return true; + } + return false; +} + +bool HasKind(const PthreadMutexattr* attr, int kind) { + int type = -1; + return pthread_mutexattr_gettype_nid_postfix(attr, &type) == 0 && type == kind; +} + +bool LocksTwice(const PthreadMutexattr* attr, int secondLock) { + PthreadMutex mutex = nullptr; + if (pthread_mutex_init_nid_postfix(&mutex, attr) != 0) return false; + const bool locked = pthread_mutex_lock_nid_postfix(&mutex) == 0 && pthread_mutex_lock_nid_postfix(&mutex) == secondLock; + if (secondLock == 0) pthread_mutex_unlock_nid_postfix(&mutex); + return locked && pthread_mutex_unlock_nid_postfix(&mutex) == 0 && pthread_mutex_destroy_nid_postfix(&mutex) == 0; +} + +void TestKind() { + Case("scePthreadMutexattrSetkind", [] { + PthreadMutexattr attr = nullptr; + if (pthread_mutexattr_init_nid_postfix(&attr) != 0) return false; + for (const int kind : {KIND_RECURSIVE, KIND_NORMAL, KIND_ADAPTIVE, KIND_ERRORCHECK}) { + if (scePthreadMutexattrSetkind(&attr, kind) != 0 || !HasKind(&attr, kind)) return false; + } + if (!LocksTwice(&attr, GUEST_EDEADLK)) return false; + if (scePthreadMutexattrSetkind(&attr, KIND_RECURSIVE) != 0 || !LocksTwice(&attr, 0)) return false; + for (const int kind : {0, 5, -1}) { + if (!Throws([&] { return scePthreadMutexattrSetkind(&attr, kind); }) || !HasKind(&attr, KIND_RECURSIVE)) return false; + } + if (pthread_mutexattr_destroy_nid_postfix(&attr) != 0) return false; + return scePthreadMutexattrSetkind(&attr, KIND_NORMAL) == SCE_FAILED_CALL && scePthreadMutexattrSetkind(nullptr, KIND_NORMAL) == SCE_FAILED_CALL; + }); + Case("scePthreadMutexattrGetkind", [] { + PthreadMutexattr attr = nullptr; + if (pthread_mutexattr_init_nid_postfix(&attr) != 0) return false; + for (const int kind : {KIND_RECURSIVE, KIND_NORMAL, KIND_ADAPTIVE, KIND_ERRORCHECK}) { + if (scePthreadMutexattrSetkind(&attr, kind) != 0 || scePthreadMutexattrGetkind(attr) != static_cast(SCE_KERNEL_ERROR_BASE + static_cast(kind))) return false; + } + if (pthread_mutexattr_destroy_nid_postfix(&attr) != 0) return false; + return scePthreadMutexattrGetkind(nullptr) == SCE_FAILED_CALL; + }); +} + +void TestRwlockattr() { + Case("pthread_rwlockattr_settype_np", [] { + PthreadRwlockattr attr = nullptr; + if (pthread_rwlockattr_init_nid_postfix(&attr) != 0) return false; + for (const int type : {1, 2}) { + if (pthread_rwlockattr_settype_np_nid_postfix(&attr, type) != 0 || scePthreadRwlockattrSettype(&attr, type) != 0) return false; + PthreadRwlock rwlock = nullptr; + if (pthread_rwlock_init_nid_postfix(&rwlock, &attr) != 0) return false; + const bool usable = pthread_rwlock_wrlock_nid_postfix(&rwlock) == 0 && pthread_rwlock_unlock_nid_postfix(&rwlock) == 0; + if (!usable || pthread_rwlock_destroy_nid_postfix(&rwlock) != 0) return false; + } + for (const int type : {0, 3, -1, static_cast(0x80000000u)}) { + if (pthread_rwlockattr_settype_np_nid_postfix(&attr, type) != GUEST_EINVAL) return false; + } + if (pthread_rwlockattr_destroy_nid_postfix(&attr) != 0) return false; + return pthread_rwlockattr_settype_np_nid_postfix(&attr, 1) == GUEST_EINVAL && pthread_rwlockattr_settype_np_nid_postfix(nullptr, 1) == GUEST_EINVAL; + }); +} + +} + +int main(int argc, char** argv) { + const std::string_view mode = argc > 1 ? argv[1] : ""; + if (mode == "rwlockattr") TestRwlockattr(); + else if (mode == "kind") TestKind(); + else return 2; + if (failures == 0) std::printf("all cases passed\n"); + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/tests/GuestKernelSysSwapcontext.cpp b/core/libs/tests/GuestKernelSysSwapcontext.cpp new file mode 100644 index 0000000000..1aa364e87e --- /dev/null +++ b/core/libs/tests/GuestKernelSysSwapcontext.cpp @@ -0,0 +1,172 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libSceLibcInternal/include/Api.hpp" +#include "prx/libc/include/general/AsmMacros.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/System/include/UserContext.hpp" +#include +#include +#include +#include +#include + +using ContextEntry = void (APS5_VABI*)(); + +extern "C" { +int APS5_VABI SwapKeepingRegisters(UserUcontext* saved, const UserUcontext* next); +int APS5_VABI SwapClobberingRegisters(UserUcontext* saved, const UserUcontext* next); +} + +asm(".text\n" + APS5_ASM_FUNCTION("SwapKeepingRegisters") + " push %rbx\n" + " push %rbp\n" + " push %r12\n" + " push %r13\n" + " push %r14\n" + " push %r15\n" + " sub $8, %rsp\n" + " movabsq $0x1111111111111111, %rbx\n" + " movabsq $0x2222222222222222, %rbp\n" + " movabsq $0x3333333333333333, %r12\n" + " movabsq $0x4444444444444444, %r13\n" + " movabsq $0x5555555555555555, %r14\n" + " movabsq $0x6666666666666666, %r15\n" + " call " APS5_ASM_SYMBOL("swapcontext_nid_postfix") "\n" + " movabsq $0x1111111111111111, %rcx\n" + " cmp %rcx, %rbx\n" + " jne 1f\n" + " movabsq $0x2222222222222222, %rcx\n" + " cmp %rcx, %rbp\n" + " jne 1f\n" + " movabsq $0x3333333333333333, %rcx\n" + " cmp %rcx, %r12\n" + " jne 1f\n" + " movabsq $0x4444444444444444, %rcx\n" + " cmp %rcx, %r13\n" + " jne 1f\n" + " movabsq $0x5555555555555555, %rcx\n" + " cmp %rcx, %r14\n" + " jne 1f\n" + " movabsq $0x6666666666666666, %rcx\n" + " cmp %rcx, %r15\n" + " je 2f\n" + "1:\n" + " mov $-100, %eax\n" + "2:\n" + " add $8, %rsp\n" + " pop %r15\n" + " pop %r14\n" + " pop %r13\n" + " pop %r12\n" + " pop %rbp\n" + " pop %rbx\n" + " ret\n" + APS5_ASM_FUNCTION("SwapClobberingRegisters") + " push %rbx\n" + " push %rbp\n" + " push %r12\n" + " push %r13\n" + " push %r14\n" + " push %r15\n" + " sub $8, %rsp\n" + " movabsq $0x0badc0de0badc0de, %rbx\n" + " mov %rbx, %rbp\n" + " mov %rbx, %r12\n" + " mov %rbx, %r13\n" + " mov %rbx, %r14\n" + " mov %rbx, %r15\n" + " call " APS5_ASM_SYMBOL("swapcontext_nid_postfix") "\n" + " add $8, %rsp\n" + " pop %r15\n" + " pop %r14\n" + " pop %r13\n" + " pop %r12\n" + " pop %rbp\n" + " pop %rbx\n" + " ret\n"); + +namespace { + +constexpr int Einval = 22; +constexpr int SigBlock = 1; +constexpr int SigSetmask = 3; +constexpr int SigUsr1 = 30; +constexpr std::size_t StackBytes = 256 * 1024; + +struct alignas(16) Stack { + unsigned char bytes[StackBytes]; +}; + +Stack g_stack; +UserUcontext g_main = {}; +UserUcontext g_coroutine = {}; +int g_steps = 0; +bool g_coroutineSawBlocked = true; + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "check failed at line %u\n", static_cast(location.line())); + std::exit(1); +} + +int Error() { + return *__error_nid_postfix(); +} + +bool Usr1Blocked() { + std::uint32_t mask[4] = {}; + Require(sigprocmask_nid_postfix(SigBlock, nullptr, mask) == 0); + return (mask[0] & (1u << (SigUsr1 - 1))) != 0; +} + +void APS5_VABI Coroutine() { + g_coroutineSawBlocked = Usr1Blocked(); + for (int step = 0; step < 3; ++step) { + ++g_steps; + Require(SwapClobberingRegisters(&g_coroutine, &g_main) == 0); + } +} + +void Arguments() { + UserUcontext context = {}; + Require(getcontext_nid_postfix(&context) == 0); + Require(swapcontext_nid_postfix(nullptr, &context) == -1 && Error() == Einval); + Require(swapcontext_nid_postfix(&context, nullptr) == -1 && Error() == Einval); + + UserUcontext saved = {}; + UserUcontext invalid = context; + invalid.mcontext.len = 0; + Require(swapcontext_nid_postfix(&saved, &invalid) == -1 && Error() == Einval); + Require(saved.mcontext.len == sizeof(UserMcontext) && saved.mcontext.rip != 0); +} + +void PingPong() { + Require(getcontext_nid_postfix(&g_coroutine) == 0); + g_coroutine.stackPointer = g_stack.bytes; + g_coroutine.stackSize = sizeof(g_stack.bytes); + g_coroutine.link = &g_main; + makecontext_nid_postfix(&g_coroutine, &Coroutine, 0); + Require(g_coroutine.mcontext.len == sizeof(UserMcontext)); + + std::uint32_t block[4] = {1u << (SigUsr1 - 1), 0, 0, 0}; + std::uint32_t previous[4] = {}; + Require(sigprocmask_nid_postfix(SigBlock, block, previous) == 0); + for (int expected = 1; expected <= 3; ++expected) { + Require(SwapKeepingRegisters(&g_main, &g_coroutine) == 0); + Require(g_steps == expected && Usr1Blocked()); + } + Require(!g_coroutineSawBlocked); + Require(SwapKeepingRegisters(&g_main, &g_coroutine) == 0); + Require(g_steps == 3 && Usr1Blocked()); + Require(sigprocmask_nid_postfix(SigSetmask, previous, nullptr) == 0); +} + +} + +int main() { + Arguments(); + PingPong(); + std::puts("guest_kernel_sys_swapcontext passed"); + return 0; +} diff --git a/core/libs/tests/GuestKernelSysThreads.cpp b/core/libs/tests/GuestKernelSysThreads.cpp new file mode 100644 index 0000000000..4134cfd6f2 --- /dev/null +++ b/core/libs/tests/GuestKernelSysThreads.cpp @@ -0,0 +1,104 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#else +#include +#endif + +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (!value) { + std::fprintf(stderr, "Kernel sys API check failed at line %u\n", location.line()); + std::abort(); + } +} + +static void* LoadLibraryFile(const std::filesystem::path& path) { +#ifdef _WIN32 + auto* module = reinterpret_cast(LoadLibraryExW(path.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH)); +#else + auto* module = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); + if (!module) std::fprintf(stderr, "%s\n", dlerror()); +#endif + Require(module != nullptr); +#ifdef _WIN32 + std::wstring loaded(32768, L'\0'); + const auto length = GetModuleFileNameW(static_cast(module), loaded.data(), static_cast(loaded.size())); + Require(length != 0 && length < loaded.size()); + loaded.resize(length); + Require(std::filesystem::equivalent(path, std::filesystem::path(loaded))); +#endif + return module; +} + +template +static Function LoadFunction(void* module, const char* name, bool patched, const char* unpatched = nullptr) { + const auto symbol = patched ? Nid::ComputeNid(name, "") : unpatched ? std::string(unpatched) : std::string(name) + "_nid_postfix"; +#ifdef _WIN32 + const auto function = reinterpret_cast(reinterpret_cast(GetProcAddress(static_cast(module), symbol.c_str()))); +#else + const auto function = reinterpret_cast(dlsym(module, symbol.c_str())); +#endif + if (!function) std::fprintf(stderr, "Missing symbol %s\n", symbol.c_str()); + Require(function != nullptr); + return function; +} + + +int main(int argc, char** argv) { + Require(argc == 4); + const auto directory = std::filesystem::absolute(argv[1]); + const bool patched = std::strcmp(argv[2], "patched") == 0; + const std::string test = argv[3]; +#ifdef __linux__ + if (test == "main_late_load") { + int (APS5_VABI* isMain)() = nullptr; + int (APS5_VABI* sceIsMain)() = nullptr; + int other = -1; + int sceOther = -1; + std::thread loader([&] { + auto* module = LoadLibraryFile(directory / "libkernel_sys.prx"); + isMain = LoadFunction(module, "pthread_main_np", patched); + sceIsMain = LoadFunction(module, "scePthreadMain", patched, "scePthreadMain"); + other = isMain(); + sceOther = sceIsMain(); + }); + loader.join(); + Require(other == 0 && isMain() == 1 && sceOther == 0 && sceIsMain() == 1); + return 0; + } +#endif + Require(test == "main"); + auto* sys = LoadLibraryFile(directory / "libkernel_sys.prx"); + LoadLibraryFile(directory / "libkernel.prx"); + auto* libc = LoadLibraryFile(directory / "libc.prx"); + const auto error = LoadFunction(libc, "__error", patched); + const auto isMain = LoadFunction(sys, "pthread_main_np", patched); + const auto sceIsMain = LoadFunction(sys, "scePthreadMain", patched, "scePthreadMain"); + *error() = 71; + int other = -1; + int sceOther = -1; + std::thread worker([&] { + other = isMain(); + sceOther = sceIsMain(); + }); + worker.join(); + Require(other == 0 && isMain() == 1 && sceOther == 0 && sceIsMain() == 1 && *error() == 71); + return 0; +} diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 4400651e3c..f3a6638b89 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -859,6 +859,59 @@ static void DisableAndDispatch() { Require(close_nid_postfix(kq) == 0); } +static void RequireNext(int kq, std::uintptr_t ident) { + KernelEvent event{}; + Require(Poll(kq, &event, 1) == 1 && event.ident == ident); +} + +static void ReportedKnotesMoveToTail() { + const int kq = kqueue_nid_postfix(); + int first[2]; + int second[2]; + int pair[2]; + Require(pipe_nid_postfix(first) == 0 && pipe_nid_postfix(second) == 0); + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + const std::uintptr_t level[3] = {static_cast(first[0]), static_cast(second[0]), static_cast(pair[0])}; + for (const auto ident : level) { + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd)) == 0); + } + Require(write_nid_postfix(first[1], "a", 1) == 1 && write_nid_postfix(second[1], "b", 1) == 1); + Require(send_nid_postfix(pair[1], "c", 1, 0) == 1); + for (int round = 0; round < 2; ++round) { + for (const auto ident : level) { + RequireNext(kq, ident); + } + } + for (const auto ident : level) { + Require(Apply(kq, Change(ident, EvfiltRead, EvDelete)) == 0); + } + + KernelEvent event{}; + for (const std::uintptr_t ident : {11, 12}) { + Require(Apply(kq, Change(ident, EvfiltUser, EvAdd | EvClear)) == 0); + Require(Apply(kq, Change(ident, EvfiltUser, 0, NoteTrigger)) == 0); + } + RequireNext(kq, 11); + Require(Apply(kq, Change(11, EvfiltUser, 0, NoteTrigger)) == 0); + RequireNext(kq, 12); + RequireNext(kq, 11); + Require(Poll(kq, &event, 1) == 0); + + for (const std::uintptr_t ident : {21, 22}) { + Require(Apply(kq, Change(ident, EvfiltUser, EvAdd | EvDispatch)) == 0); + Require(Apply(kq, Change(ident, EvfiltUser, 0, NoteTrigger)) == 0); + } + RequireNext(kq, 21); + Require(Apply(kq, Change(21, EvfiltUser, EvEnable, NoteTrigger)) == 0); + RequireNext(kq, 22); + RequireNext(kq, 21); + Require(Poll(kq, &event, 1) == 0); + + for (const int descriptor : {first[0], first[1], second[0], second[1], pair[0], pair[1], kq}) { + Require(close_nid_postfix(descriptor) == 0); + } +} + static void Errors() { const int kq = kqueue_nid_postfix(); KernelEvent events[4]{}; @@ -1196,6 +1249,102 @@ static void DeniedParentWatch() { Require(std::filesystem::remove_all(path) == 2); } #endif + +#ifdef __linux__ +static std::size_t InotifyWatchCount() { + std::size_t count = 0; + std::error_code error; + for (const auto& entry : std::filesystem::directory_iterator("/proc/self/fd", error)) { + std::error_code linkError; + const auto target = std::filesystem::read_symlink(entry.path(), linkError); + if (!linkError && target.string().find("inotify") != std::string::npos) ++count; + } + return count; +} +#endif + +static void VnodeOneshotWatchStops() { + const auto path = TemporaryName("oneshot"); + const int file = open_nid_postfix(path.c_str(), ReadWrite | Create, 0644); + Require(file >= 0); + const int kq = kqueue_nid_postfix(); + int marker = 0; + KernelEvent events[4]{}; + const auto ident = static_cast(file); +#ifdef __linux__ + auto watchesBefore = InotifyWatchCount(); + for (int attempt = 0; attempt < 100; ++attempt) { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + const auto settled = InotifyWatchCount(); + if (settled == watchesBefore) break; + watchesBefore = settled; + } +#endif + Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd | EvOneshot, NoteDelete | NoteWrite, 0, &marker)) == 0); + Require(write_nid_postfix(file, "abc", 3) == 3); + Require(Wait(kq, events, 4) == 1); + Require(events[0].ident == ident && events[0].filter == EvfiltVnode && (events[0].flags & EvOneshot) != 0 && events[0].udata == &marker); + Require(write_nid_postfix(file, "def", 3) == 3); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + Require(Poll(kq, events, 4) == 0); + RequireFailure(Apply(kq, Change(ident, EvfiltVnode, EvDelete)), GuestEnoent); +#ifdef __linux__ + for (int attempt = 0; attempt < 100 && InotifyWatchCount() > watchesBefore; ++attempt) std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(InotifyWatchCount() <= watchesBefore); +#endif + Require(close_nid_postfix(file) == 0); + Require(close_nid_postfix(kq) == 0); + Require(std::filesystem::remove(path)); +} + +static void TimerDisableEnableRearm() { + const int kq = kqueue_nid_postfix(); + int marker = 0; + KernelEvent events[4]{}; + Require(Apply(kq, Change(1, EvfiltTimer, EvAdd, NoteUseconds, 50000, &marker)) == 0); + Require(Apply(kq, Change(1, EvfiltTimer, EvDisable, 0, 0, &marker)) == 0); + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + Require(Apply(kq, Change(1, EvfiltTimer, EvEnable, 0, 0, &marker)) == 0); + Require(Wait(kq, events, 4) == 1); + Require(events[0].ident == 1 && events[0].filter == EvfiltTimer && events[0].udata == &marker); + Require(events[0].data >= 2 && events[0].data <= 6); + Require(Wait(kq, events, 4) == 1); + Require(events[0].data >= 1 && events[0].data <= 2); + Require(Throws([kq] { Apply(kq, Change(1, EvfiltTimer, EvAdd | EvEnable)); })); + Require(Apply(kq, Change(1, EvfiltTimer, EvDelete)) == 0); + Require(close_nid_postfix(kq) == 0); +} + +static void PeriodicTimerFiresTwice() { + const int kq = kqueue_nid_postfix(); + KernelEvent events[4]{}; + Require(Apply(kq, Change(1, EvfiltTimer, EvAdd, NoteUseconds, 50000)) == 0); + Require(Wait(kq, events, 4) == 1); + Require(Wait(kq, events, 4) == 1); + Require(Apply(kq, Change(1, EvfiltTimer, EvDelete)) == 0); + Require(close_nid_postfix(kq) == 0); +} + +#ifdef _WIN32 +static void RecycleBinDeleteWatchedFile() { + const auto directory = TemporaryName("deleted"); + std::filesystem::create_directories(directory + "\\$Extend\\$Deleted"); + const auto path = directory + "\\watched"; + const int file = open_nid_postfix(path.c_str(), ReadWrite | Create, 0644); + Require(file >= 0); + const int kq = kqueue_nid_postfix(); + KernelEvent events[4]{}; + const auto ident = static_cast(file); + Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd | EvClear, NoteDelete | NoteRename)) == 0); + Require(sceKernelRename(path.c_str(), (directory + "\\$Extend\\$Deleted\\moved").c_str()) == SceOk); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + Require(Poll(kq, events, 4) == 1 && events[0].ident == ident && (events[0].fflags & NoteDelete) != 0); + Require(close_nid_postfix(file) == 0); + Require(close_nid_postfix(kq) == 0); + std::filesystem::remove_all(directory); +} +#endif + static void Timeout() { const int kq = kqueue_nid_postfix(); KernelEvent event{}; @@ -1291,11 +1440,18 @@ int main() { EdgeTriggeredDispatch(); #endif DisableAndDispatch(); + ReportedKnotesMoveToTail(); Errors(); LowWaterMarks(); FileWatch(); #ifndef _WIN32 RenameAndDeleteWatchedFile(); + VnodeOneshotWatchStops(); + PeriodicTimerFiresTwice(); + TimerDisableEnableRearm(); +#endif +#ifdef _WIN32 + RecycleBinDeleteWatchedFile(); #endif #if !defined(_WIN32) && !defined(__APPLE__) MoveDirectoryEntries(); diff --git a/core/libs/tests/GuestLibcThreads.cpp b/core/libs/tests/GuestLibcThreads.cpp index 1d2b3d7dff..adb6ec98ee 100644 --- a/core/libs/tests/GuestLibcThreads.cpp +++ b/core/libs/tests/GuestLibcThreads.cpp @@ -5,15 +5,15 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include +#include #include +#include #include #include -extern "C" { - -} static void Require(bool value) { if (!value) std::abort(); } @@ -28,6 +28,7 @@ static void RequireThrows(TCall call) { } constexpr int success = 0; +constexpr int timedout = 2; constexpr int busy = 3; constexpr int plain = 0x01; constexpr int tryable = 0x02; @@ -40,6 +41,14 @@ static void* CreateMutex(int type) { return mutex; } +static Xtime RelativeTime(int milliseconds) { + const auto point = std::chrono::system_clock::now() + std::chrono::milliseconds(milliseconds); + const auto since = point.time_since_epoch(); + const auto seconds = std::chrono::duration_cast(since); + const auto nanos = std::chrono::duration_cast(since - seconds); + return {seconds.count(), nanos.count()}; +} + static void MutexBasics() { void* mutex = CreateMutex(tryable); Require(_Mtx_lock_nid_postfix(&mutex) == success); @@ -110,6 +119,37 @@ static void RecursiveMutex() { } +static void TimedMutex() { + void* mutex = CreateMutex(tryable); + Xtime future = RelativeTime(60000); + Require(_Mtx_timedlock_nid_postfix(&mutex, &future) == success); + Require(_Mtx_timedlock_nid_postfix(&mutex, &future) == busy); + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + + Require(_Mtx_lock_nid_postfix(&mutex) == success); + int result = -1; + std::thread worker([&] { + Xtime deadline = RelativeTime(30); + result = _Mtx_timedlock_nid_postfix(&mutex, &deadline); + }); + worker.join(); + Require(result == timedout); + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + + void* empty = nullptr; + RequireThrows([&] { _Mtx_timedlock_nid_postfix(&empty, &future); }); + RequireThrows([&] { _Mtx_timedlock_nid_postfix(nullptr, &future); }); + RequireThrows([&] { _Mtx_timedlock_nid_postfix(&mutex, nullptr); }); + _Mtx_destroy_nid_postfix(&mutex); + + void* recursiveMutex = CreateMutex(recursive); + Require(_Mtx_timedlock_nid_postfix(&recursiveMutex, &future) == success); + Require(_Mtx_timedlock_nid_postfix(&recursiveMutex, &future) == success); + Require(_Mtx_unlock_nid_postfix(&recursiveMutex) == success); + Require(_Mtx_unlock_nid_postfix(&recursiveMutex) == success); + _Mtx_destroy_nid_postfix(&recursiveMutex); +} + static void MutualExclusion() { void* mutex = CreateMutex(tryable); long counter = 0; @@ -202,7 +242,10 @@ struct HostPadThread { void* result = nullptr; }; -static int APS5_VABI HostPthreadCreate(void** thread, const void*, void* (APS5_VABI* entry)(void*), void* arg) { +static const void* lastCreateAttrSlot = nullptr; + +static int APS5_VABI HostPthreadCreate(void** thread, const void* attrSlot, void* (APS5_VABI* entry)(void*), void* arg) { + lastCreateAttrSlot = attrSlot; auto* handle = new HostPadThread{}; handle->worker = std::thread([handle, entry, arg] { handle->result = entry(arg); }); *thread = handle; @@ -232,6 +275,80 @@ static unsigned APS5_VABI TestPadGo(GuestPad* base) { return 42; } +static void RequireExit(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "CRT exit lifecycle: %s\n", message); + std::abort(); + } +} + +struct CrtExitState { + int code = 0; + int key = -1; + std::barrier<>* ready = nullptr; + void* parent = nullptr; + void* self = nullptr; + std::atomic destructors{0}; + std::atomic returned{false}; +}; + +static void APS5_VABI CrtExitDestructor(void* value) { + auto& state = *static_cast(value); + RequireExit(_Thrd_current_nid_postfix() == state.self, "identity during destructor"); + RequireExit(_Tss_get_nid_postfix(state.key) == nullptr, "value cleared before destructor"); + if (state.destructors.fetch_add(1) == 0) + RequireExit(_Tss_set_nid_postfix(state.key, &state) == 0, "reinstall value for another cleanup pass"); +} + +static auto volatile crtExit = &_Thrd_exit_nid_postfix; + +static void* APS5_VABI CrtExitEntry(void* value) { + auto& state = *static_cast(value); + state.self = _Thrd_current_nid_postfix(); + RequireExit(state.self && _Thrd_equal_nid_postfix(state.self, state.self) == 1, "current identity"); + RequireExit(_Thrd_equal_nid_postfix(state.self, state.parent) == 0, "different parent identity"); + RequireExit(_Tss_get_nid_postfix(state.key) == nullptr, "child starts with empty value"); + RequireExit(_Tss_set_nid_postfix(state.key, &state) == 0, "set child value"); + state.ready->arrive_and_wait(); + RequireExit(_Tss_get_nid_postfix(state.key) == &state, "child value isolation"); + crtExit(state.code); + state.returned = true; + return nullptr; +} + +static void ThreadExitLifecycle() { + constexpr std::array codes{0, 42, -1, std::numeric_limits::min(), std::numeric_limits::max()}; + constexpr std::array expected{0, 42, 0xffffffffu, 0x80000000u, 0x7fffffffu}; + std::barrier ready(6); + std::array states; + std::array threads{}; + int key = -1, parentValue = 31; + void* parent = _Thrd_current_nid_postfix(); + RequireExit(_Tss_create_nid_postfix(&key, CrtExitDestructor) == 0, "create destructor key"); + RequireExit(_Tss_set_nid_postfix(key, &parentValue) == 0, "set parent value"); + for (unsigned i = 0; i < states.size(); ++i) { + states[i].code = codes[i]; + states[i].key = key; + states[i].ready = &ready; + states[i].parent = parent; + RequireExit(scePthreadCreate(&threads[i], nullptr, CrtExitEntry, &states[i], "CRT exit audit") == 0, "create child"); + } + RequireExit(_Tss_get_nid_postfix(key) == &parentValue, "parent isolated before release"); + ready.arrive_and_wait(); + for (unsigned i = 0; i < states.size(); ++i) { + void* result = nullptr; + RequireExit(scePthreadJoin(threads[i], &result) == 0, "join exiting child"); + RequireExit(reinterpret_cast(result) == expected[i], "full zero-extended exit result"); + RequireExit(!states[i].returned, "exit must not return"); + RequireExit(states[i].destructors == 2, "reinstalled destructor runs twice before join returns"); + RequireExit(states[i].self == threads[i], "created and current identity match"); + } + RequireExit(_Tss_get_nid_postfix(key) == &parentValue, "parent isolated after children exit"); + RequireExit(_Tss_delete_nid_postfix(key) == 0, "delete key"); + RequireExit(_Tss_get_nid_postfix(key) == nullptr, "deleted key value unavailable"); + std::puts("PASS CRT lifecycle: five concurrent exit codes, full return width, identity, parent/child isolation, two destructor passes, key deletion"); +} + static void PadLaunchAndJoin() { unsigned (APS5_VABI* vtable[1])(GuestPad*) = {TestPadGo}; TestPad unregPad{}; @@ -263,6 +380,7 @@ static void PadLaunchAndJoin() { _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(reinterpret_cast(&pad), &thread); Require(thread != nullptr && pad.started && pad.ran.load()); + Require(lastCreateAttrSlot == nullptr); _ZNSt4_PadD2Ev_nid_postfix(reinterpret_cast(&pad)); int code = 0; @@ -273,6 +391,32 @@ static void PadLaunchAndJoin() { RegisterPthreadJoin_nid_no_patch(nullptr); } +static void PadLaunchWithAttributes() { + unsigned (APS5_VABI* vtable[1])(GuestPad*) = {TestPadGo}; + RegisterPthreadCreate_nid_no_patch(HostPthreadCreate); + RegisterPthreadJoin_nid_no_patch(HostPthreadJoin); + + TestPad pad{}; + pad.vtable = vtable; + _ZNSt4_PadC2Ev_nid_postfix(reinterpret_cast(&pad)); + + int attribute = 0; + void* attrSlot = &attribute; + void* thread = nullptr; + lastCreateAttrSlot = nullptr; + _ZNSt4_Pad7_LaunchEPP12pthread_attrPP7pthread_nid_postfix(reinterpret_cast(&pad), &attrSlot, &thread); + Require(thread != nullptr && pad.started && pad.ran.load()); + Require(attrSlot == &attribute); + Require(lastCreateAttrSlot == &attrSlot); + + _ZNSt4_PadD2Ev_nid_postfix(reinterpret_cast(&pad)); + int code = 0; + Require(_Thrd_join_nid_postfix(thread, &code) == success && code == 42); + + RegisterPthreadCreate_nid_no_patch(nullptr); + RegisterPthreadJoin_nid_no_patch(nullptr); +} + static void PadDetach() { unsigned (APS5_VABI* vtable[1])(GuestPad*) = {TestPadGo}; RegisterPthreadDetach_nid_no_patch(nullptr); @@ -300,6 +444,7 @@ static void PadDetach() { static void KernelKeys() { Require(_Thrd_current_nid_postfix() == scePthreadSelf()); + Require(_Thrd_id_nid_postfix() == scePthreadSelf()); int key = -1; Require(_Tss_create_nid_postfix(&key, nullptr) == success && key >= 0); int value = 0; @@ -346,10 +491,12 @@ static void ForwardedCalls() { Require(_Tss_set_nid_postfix(8, &flag) == success && forwardLog.key == 8 && forwardLog.value == &flag); Require(_Thrd_equal_nid_postfix(&flag, &key) == 1 && forwardLog.first == &flag && forwardLog.second == &key); Require(_Thrd_current_nid_postfix() == &forwardLog.key); + Require(_Thrd_id_nid_postfix() == &forwardLog.key); Require(_Mtx_current_owns_nid_postfix(nullptr) == 1); RegisterThreadForwards_nid_no_patch(nullptr); RequireThrows([] { _Thrd_current_nid_postfix(); }); + RequireThrows([] { _Thrd_id_nid_postfix(); }); RequireThrows([] { _Tss_get_nid_postfix(0); }); } @@ -435,15 +582,18 @@ static void ThreadHelpers() { } int main() { + ThreadExitLifecycle(); KernelKeys(); VersionedSync(); ForwardedCalls(); MutexBasics(); RecursiveMutex(); + TimedMutex(); MutualExclusion(); ConditionBroadcast(); ConditionSignal(); PadLaunchAndJoin(); + PadLaunchWithAttributes(); PadDetach(); ThreadHelpers(); } diff --git a/core/libs/tests/GuestLibreSsl3Digest.cpp b/core/libs/tests/GuestLibreSsl3Digest.cpp index dcddacf406..c2b6bf80a8 100644 --- a/core/libs/tests/GuestLibreSsl3Digest.cpp +++ b/core/libs/tests/GuestLibreSsl3Digest.cpp @@ -14,8 +14,8 @@ static void Require(bool condition, std::source_location location = std::source_ std::abort(); } -template static void RequireThrows(TAction action, std::source_location location = std::source_location::current()) { - try { action(); } catch (const std::exception&) { return; } +template static void RequireInvalidArgument(TAction action, std::source_location location = std::source_location::current()) { + try { action(); } catch (const std::invalid_argument&) { return; } catch (const std::exception&) {} Require(false, location); } @@ -42,9 +42,9 @@ int main() { Require(SHA1_nid_postfix(abc, 3, digest) == digest && Hex(digest, 20) == "a9993e364706816aba3e25717850c26c9cd0d89d" && digest[20] == 0xee); Require(SHA1_nid_postfix(nullptr, 0, digest) == digest && Hex(digest, 20) == "da39a3ee5e6b4b0d3255bfef95601890afd80709"); Require(SHA1_nid_postfix(many, million.size(), digest) == digest && Hex(digest, 20) == "34aa973cd4c4daa4f61eeb2bdbad27316534016f"); - RequireThrows([&] { MD5_nid_postfix(abc, 3, nullptr); }); - RequireThrows([&] { SHA1_nid_postfix(abc, 3, nullptr); }); - RequireThrows([&] { SHA1_nid_postfix(nullptr, 1, digest); }); + RequireInvalidArgument([&] { MD5_nid_postfix(abc, 3, nullptr); }); + RequireInvalidArgument([&] { SHA1_nid_postfix(abc, 3, nullptr); }); + RequireInvalidArgument([&] { SHA1_nid_postfix(nullptr, 1, digest); }); std::puts("libSceLibreSsl3 digest tests passed"); } diff --git a/core/libs/tests/GuestLibreSslAlert.cpp b/core/libs/tests/GuestLibreSslAlert.cpp new file mode 100644 index 0000000000..73741f8c45 --- /dev/null +++ b/core/libs/tests/GuestLibreSslAlert.cpp @@ -0,0 +1,62 @@ +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include +#include +#include +#include +#include + +void Check(int value, const char* expected) { + const char* actual = SSL_alert_desc_string_long_nid_postfix(value); + if (actual == nullptr || std::strcmp(actual, expected) != 0) { + std::fprintf(stderr, "LibreSSL alert %d: expected %s, got %s\n", value, expected, actual ? actual : "(null)"); + std::abort(); + } +} + +int main() { + std::array expected; + expected.fill("unknown"); + expected[0] = "close notify"; + expected[10] = "unexpected_message"; + expected[20] = "bad record mac"; + expected[30] = "decompression failure"; + expected[40] = "handshake failure"; + expected[41] = "no certificate"; + expected[42] = "bad certificate"; + expected[43] = "unsupported certificate"; + expected[44] = "certificate revoked"; + expected[45] = "certificate expired"; + expected[46] = "certificate unknown"; + expected[47] = "illegal parameter"; + expected[21] = "decryption failed"; + expected[22] = "record overflow"; + expected[48] = "unknown CA"; + expected[49] = "access denied"; + expected[50] = "decode error"; + expected[51] = "decrypt error"; + expected[60] = "export restriction"; + expected[70] = "protocol version"; + expected[71] = "insufficient security"; + expected[80] = "internal error"; + expected[90] = "user canceled"; + expected[100] = "no renegotiation"; + expected[110] = "unsupported extension"; + expected[111] = "certificate unobtainable"; + expected[112] = "unrecognized name"; + expected[113] = "bad certificate status response"; + expected[114] = "bad certificate hash value"; + expected[115] = "unknown PSK identity"; + for (int code = 0; code < 256; ++code) { + Check(code, expected[code]); + Check(0x100 | code, expected[code]); + Check(0x200 | code, expected[code]); + Check(0x123400 | code, expected[code]); + Check(code - 256, expected[code]); + } + Check(std::numeric_limits::min(), "close notify"); + Check(std::numeric_limits::max(), "unknown"); + const char* retained = SSL_alert_desc_string_long_nid_postfix(40); + Check(42, "bad certificate"); + if (std::strcmp(retained, "handshake failure") != 0) std::abort(); + return 0; +} diff --git a/core/libs/tests/GuestLibreSslCipher.cpp b/core/libs/tests/GuestLibreSslCipher.cpp new file mode 100644 index 0000000000..366264cc99 --- /dev/null +++ b/core/libs/tests/GuestLibreSslCipher.cpp @@ -0,0 +1,97 @@ +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include +#include +#include +#include +#include +#include +#include + +struct alignas(8) RawCipher { + std::array prefix; + std::int32_t strength; + std::int32_t algorithm; +}; + +static_assert(sizeof(RawCipher) == 88); +static_assert(offsetof(RawCipher, strength) == 80); +static_assert(offsetof(RawCipher, algorithm) == 84); + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "failed: %s\n", message); + std::abort(); +} + +static void RequireString(const char* actual, const char* expected) { + Require(actual && std::strcmp(actual, expected) == 0, expected); +} + +int main() { + struct { std::int32_t before, bits, after; } output {0x12345678, -777, 0x23456789}; + Require(SSL_CIPHER_get_bits_nid_postfix(nullptr, &output.bits) == 0, "null cipher has zero strength"); + Require(output.bits == -777, "null cipher leaves the optional output untouched"); + Require(SSL_CIPHER_get_bits_nid_postfix(nullptr, nullptr) == 0, "both pointers may be null"); + RequireString(SSL_CIPHER_get_name_nid_postfix(nullptr), "(NONE)"); + RequireString(SSL_CIPHER_get_version_nid_postfix(nullptr), "(NONE)"); + + struct { std::uint64_t before; RawCipher cipher; std::uint64_t after; } guarded; + std::memset(&guarded, 0xa5, sizeof(guarded)); + RawCipher& cipher = guarded.cipher; + const char customName[] = "arbitrary caller-provided cipher"; + const char* name = customName; + std::memcpy(cipher.prefix.data() + 8, &name, sizeof(name)); + Require(SSL_CIPHER_get_name_nid_postfix(&cipher) == name, "name pointer is returned without copying or a registry lookup"); + name = nullptr; + std::memcpy(cipher.prefix.data() + 8, &name, sizeof(name)); + Require(SSL_CIPHER_get_name_nid_postfix(&cipher) == nullptr, "null name field is preserved"); + name = customName; + std::memcpy(cipher.prefix.data() + 8, &name, sizeof(name)); + + constexpr std::int32_t bitPairs[][2] { + {0, 0}, {40, 128}, {56, 64}, {112, 168}, {128, 128}, {192, 192}, {256, 256}, + {-1, -2}, {std::numeric_limits::min(), std::numeric_limits::max()}, + {std::numeric_limits::max(), std::numeric_limits::min()}, + }; + for (const auto& pair : bitPairs) { + cipher.strength = pair[0]; + cipher.algorithm = pair[1]; + const auto snapshot = guarded; + output.bits = -777; + Require(SSL_CIPHER_get_bits_nid_postfix(&cipher, &output.bits) == pair[0], "effective strength is distinct from algorithm bits"); + Require(output.bits == pair[1], "algorithm bits use the 32-bit field at guest offset 84"); + Require(output.before == 0x12345678 && output.after == 0x23456789, "output writes exactly four bytes"); + Require(SSL_CIPHER_get_bits_nid_postfix(&cipher, nullptr) == pair[0], "optional output may be omitted"); + Require(std::memcmp(&guarded, &snapshot, sizeof(guarded)) == 0, "queries preserve the cipher and its guards"); + } + + constexpr std::uint64_t lowIds[] {0, 1, 0x35, 0xc02f, 0xffffff}; + for (std::uint64_t version = 0; version < 256; ++version) { + for (const auto low : lowIds) { + const std::uint64_t id = (version << 24) | low; + std::memcpy(cipher.prefix.data() + 16, &id, sizeof(id)); + RequireString(SSL_CIPHER_get_version_nid_postfix(&cipher), version == 3 ? "TLSv1/SSLv3" : "unknown"); + } + } + constexpr std::uint64_t wideIds[] {0x103000035ULL, 0x800000000300c02fULL, 0xffffffff03000000ULL, + std::numeric_limits::max()}; + for (const auto id : wideIds) { + std::memcpy(cipher.prefix.data() + 16, &id, sizeof(id)); + RequireString(SSL_CIPHER_get_version_nid_postfix(&cipher), "unknown"); + } + + cipher.strength = 40; + cipher.algorithm = 128; + Require(SSL_CIPHER_get_bits_nid_postfix(&cipher, &cipher.algorithm) == 40, "output may alias the algorithm field"); + Require(cipher.algorithm == 128, "self-alias preserves algorithm bits"); + Require(SSL_CIPHER_get_bits_nid_postfix(&cipher, &cipher.strength) == 128, "algorithm output is written before strength is read"); + Require(cipher.strength == 128, "aliased output receives algorithm bits"); + + const auto snapshot = guarded; + const std::int32_t valid = 0; + std::memcpy(cipher.prefix.data(), &valid, sizeof(valid)); + Require(SSL_CIPHER_get_name_nid_postfix(&cipher) == customName, "valid flag does not gate name access"); + Require(SSL_CIPHER_get_bits_nid_postfix(&cipher, nullptr) == 128, "valid flag does not gate strength access"); + Require(guarded.before == snapshot.before && guarded.after == snapshot.after, "input guards remain intact"); + return 0; +} diff --git a/core/libs/tests/GuestLibreSslCryptoLocks.cpp b/core/libs/tests/GuestLibreSslCryptoLocks.cpp new file mode 100644 index 0000000000..b29b03828d --- /dev/null +++ b/core/libs/tests/GuestLibreSslCryptoLocks.cpp @@ -0,0 +1,51 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +static void Require(bool value) { if (!value) std::abort(); } + +template +static void RequireThrows(TAction action) { + try { action(); } catch (const std::invalid_argument&) { return; } + std::abort(); +} + +static void APS5_VABI Locking(int, int, const char*, int) {} +static int APS5_VABI AddLock(int*, int, int, const char*, int) { return 0; } +static void APS5_VABI ThreadId(CryptoThreadId*) {} +static void APS5_VABI OtherThreadId(CryptoThreadId*) {} + +static void CheckThreadIdCallback() { + Require(CRYPTO_THREADID_set_callback_nid_postfix(nullptr) == 1); + Require(CRYPTO_THREADID_set_callback_nid_postfix(nullptr) == 1); + Require(CRYPTO_THREADID_set_callback_nid_postfix(reinterpret_cast(&ThreadId)) == 1); + Require(CRYPTO_THREADID_set_callback_nid_postfix(reinterpret_cast(&OtherThreadId)) == 0); + Require(CRYPTO_THREADID_set_callback_nid_postfix(reinterpret_cast(&ThreadId)) == 0); + Require(CRYPTO_THREADID_set_callback_nid_postfix(nullptr) == 0); +} + +static void CheckThreadIdNumeric() { + CryptoThreadId id{}; + std::memset(&id, 0xaa, sizeof(id)); + CRYPTO_THREADID_set_numeric_nid_postfix(&id, 0x1234567890abcdefull); + Require(id.ptr == nullptr && id.val == 0x1234567890abcdefull); + std::memset(&id, 0x55, sizeof(id)); + CRYPTO_THREADID_set_numeric_nid_postfix(&id, 0); + unsigned char zero[sizeof(id)]{}; + Require(std::memcmp(&id, zero, sizeof(id)) == 0); + RequireThrows([] { CRYPTO_THREADID_set_numeric_nid_postfix(nullptr, 1); }); +} + +int main() { + Require(CRYPTO_num_locks_nid_postfix() == 41); + CRYPTO_set_locking_callback_nid_postfix(reinterpret_cast(&Locking)); + CRYPTO_set_locking_callback_nid_postfix(nullptr); + CRYPTO_set_add_lock_callback_nid_postfix(reinterpret_cast(&AddLock)); + CRYPTO_set_add_lock_callback_nid_postfix(nullptr); + CheckThreadIdCallback(); + CheckThreadIdNumeric(); + return 0; +} diff --git a/core/libs/tests/GuestLibreSslDigest.cpp b/core/libs/tests/GuestLibreSslDigest.cpp index ba0ae9d3e0..e207d3ec6c 100644 --- a/core/libs/tests/GuestLibreSslDigest.cpp +++ b/core/libs/tests/GuestLibreSslDigest.cpp @@ -165,11 +165,12 @@ int main() { std::memcpy(context + 108, &length, 4); std::memset(digest, 0xee, sizeof(digest)); Require(SHA256_Final_nid_postfix(digest, reinterpret_cast(context)) == 1 && Hex(digest, 16) == "e3b0c44298fc1c149afbf4c8996fb924" && digest[16] == 0xee, "SHA256_Final truncates to the digest length"); - SHA512_Init_nid_postfix(reinterpret_cast(context)); - - length = 32; - std::memcpy(context + 212, &length, 4); - RequireThrows([&] { SHA512_Final_nid_postfix(digest, reinterpret_cast(context)); }, "SHA-512/256 digest length"); + for (std::uint32_t truncated : {28u, 32u}) { + SHA512_Init_nid_postfix(reinterpret_cast(context)); + std::memcpy(context + 212, &truncated, 4); + std::memset(digest, 0xee, sizeof(digest)); + Require(SHA512_Final_nid_postfix(digest, reinterpret_cast(context)) == 0 && digest[0] == 0xee && digest[63] == 0xee, "SHA512_Final with a SHA-512/t digest length"); + } SHA256_Init_nid_postfix(reinterpret_cast(context)); length = 64; @@ -179,6 +180,8 @@ int main() { const auto* abc = reinterpret_cast("abc"); Require(SHA256_nid_postfix(abc, 3, digest) == digest && Hex(digest, 32) == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", "SHA256 one-shot"); Require(SHA256_nid_postfix(nullptr, 0, digest) == digest && Hex(digest, 32) == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "SHA256 one-shot of nothing"); - RequireThrows([&] { SHA256_nid_postfix(abc, 3, nullptr); }, "SHA256 one-shot without a digest buffer"); + unsigned char* shared = SHA256_nid_postfix(abc, 3, nullptr); + Require(shared && shared != digest && Hex(shared, 32) == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", "SHA256 one-shot into the static buffer"); + Require(SHA256_nid_postfix(nullptr, 0, nullptr) == shared && Hex(shared, 32) == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "SHA256 one-shot reuses the static buffer"); std::puts("libSceLibreSsl digest tests passed"); } diff --git a/core/libs/tests/GuestLibreSslError.cpp b/core/libs/tests/GuestLibreSslError.cpp new file mode 100644 index 0000000000..f81ec64d6b --- /dev/null +++ b/core/libs/tests/GuestLibreSslError.cpp @@ -0,0 +1,69 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" +#include "prx/libSceLibreSsl/include/Error.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +static void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "failed: %s\n", what); + std::abort(); +} + +static std::uint64_t Pack(std::uint64_t library, std::uint64_t function, std::uint64_t reason) { + return library << 24 | function << 12 | reason; +} + +int main() { + Require(ERR_get_error_nid_postfix() == 0, "empty queue get"); + Require(ERR_peek_error_nid_postfix() == 0, "empty queue peek"); + + LibreSslPutError_nid_no_patch(3, 100, 65); + LibreSslPutError_nid_no_patch(4, 101, 66); + LibreSslPutError_nid_no_patch(20, 102, 67); + Require(ERR_peek_error_nid_postfix() == Pack(3, 100, 65), "peek returns the earliest error"); + Require(ERR_peek_error_nid_postfix() == Pack(3, 100, 65), "peek keeps the earliest error"); + Require(ERR_get_error_nid_postfix() == Pack(3, 100, 65), "get returns the earliest error"); + Require(ERR_get_error_nid_postfix() == Pack(4, 101, 66), "get returns the second error"); + Require(ERR_peek_error_nid_postfix() == Pack(20, 102, 67), "peek follows get"); + Require(ERR_get_error_nid_postfix() == Pack(20, 102, 67), "get returns the third error"); + Require(ERR_get_error_nid_postfix() == 0, "drained queue get"); + Require(ERR_peek_error_nid_postfix() == 0, "drained queue peek"); + + for (int i = 1; i <= 16; ++i) LibreSslPutError_nid_no_patch(i, 0, i); + for (int i = 2; i <= 16; ++i) Require(ERR_get_error_nid_postfix() == Pack(i, 0, i), "full queue drops the oldest error"); + Require(ERR_get_error_nid_postfix() == 0, "full queue holds 15 errors"); + + LibreSslPutError_nid_no_patch(0x1ff, 0x1fff, 0x1fff); + Require(ERR_get_error_nid_postfix() == 0xFFFFFFFFu, "fields are masked and the code keeps 32 bits"); + + LibreSslPutError_nid_no_patch(5, 1, 2); + LibreSslPutError_nid_no_patch(6, 1, 2); + ERR_clear_error_nid_postfix(); + Require(ERR_peek_error_nid_postfix() == 0, "clear empties the queue"); + LibreSslPutError_nid_no_patch(7, 1, 2); + Require(ERR_get_error_nid_postfix() == Pack(7, 1, 2), "queue works after clear"); + + LibreSslPutError_nid_no_patch(8, 1, 2); + bool otherEmpty = false; + bool otherOwn = false; + std::thread other([&] { + otherEmpty = ERR_peek_error_nid_postfix() == 0; + LibreSslPutError_nid_no_patch(9, 1, 2); + otherOwn = ERR_get_error_nid_postfix() == Pack(9, 1, 2); + }); + other.join(); + Require(otherEmpty, "another thread starts with an empty queue"); + Require(otherOwn, "another thread reads its own error"); + Require(ERR_get_error_nid_postfix() == Pack(8, 1, 2), "this thread keeps its error"); + Require(ERR_get_error_nid_postfix() == 0, "the other thread's error stays on that thread"); + + LibreSslPutError_nid_no_patch(10, 1, 2); + ERR_remove_thread_state_nid_postfix(nullptr); + Require(ERR_peek_error_nid_postfix() == 0, "removing the thread state empties the queue"); + + std::puts("guest_libressl_error: ok"); + return 0; +} diff --git a/core/libs/tests/GuestLibreSslOcsp.cpp b/core/libs/tests/GuestLibreSslOcsp.cpp new file mode 100644 index 0000000000..f0c3f82ad8 --- /dev/null +++ b/core/libs/tests/GuestLibreSslOcsp.cpp @@ -0,0 +1,46 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +using StringFunction = const char* (APS5_VABI*)(std::int64_t); + +void Check(StringFunction function, const char* name, std::int64_t value, const char* expected) { + const char* actual = function(value); + if (actual == nullptr || std::strcmp(actual, expected) != 0) { + std::fprintf(stderr, "%s(%lld): expected %s, got %s\n", name, static_cast(value), expected, actual ? actual : "(null)"); + std::abort(); + } +} + +int main() { + constexpr auto Unknown = "(UNKNOWN)"; + constexpr std::int64_t Outside[] {-1, 4, 7, 9, 0x100, 0x100000000, std::numeric_limits::min(), std::numeric_limits::max()}; + + const char* responses[] {"successful", "malformedrequest", "internalerror", "trylater", nullptr, "sigrequired", "unauthorized"}; + for (std::int64_t code = 0; code < 7; ++code) { + Check(OCSP_response_status_str_nid_postfix, "OCSP_response_status_str", code, responses[code] ? responses[code] : Unknown); + } + + const char* certificates[] {"good", "revoked", "unknown"}; + for (std::int64_t code = 0; code < 3; ++code) { + Check(OCSP_cert_status_str_nid_postfix, "OCSP_cert_status_str", code, certificates[code]); + } + + const char* reasons[] {"unspecified", "keyCompromise", "cACompromise", "affiliationChanged", "superseded", "cessationOfOperation", "certificateHold", nullptr, "removeFromCRL"}; + for (std::int64_t code = 0; code < 9; ++code) { + Check(OCSP_crl_reason_str_nid_postfix, "OCSP_crl_reason_str", code, reasons[code] ? reasons[code] : Unknown); + } + + for (const auto value : Outside) { + Check(OCSP_response_status_str_nid_postfix, "OCSP_response_status_str", value, Unknown); + Check(OCSP_cert_status_str_nid_postfix, "OCSP_cert_status_str", value, Unknown); + if (value != 4) Check(OCSP_crl_reason_str_nid_postfix, "OCSP_crl_reason_str", value, Unknown); + } + Check(OCSP_response_status_str_nid_postfix, "OCSP_response_status_str", 0x100000000 | 1, Unknown); + Check(OCSP_crl_reason_str_nid_postfix, "OCSP_crl_reason_str", -0x100000000 | 8, Unknown); + return 0; +} diff --git a/core/libs/tests/GuestLibreSslStack.cpp b/core/libs/tests/GuestLibreSslStack.cpp new file mode 100644 index 0000000000..503805789d --- /dev/null +++ b/core/libs/tests/GuestLibreSslStack.cpp @@ -0,0 +1,158 @@ +#include "prx/libSceLibreSsl/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 +#include +#include + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "LibreSSL stack check failed at line %u\n", location.line()); + std::abort(); +} + +template static void RequireThrows(TAction action, std::source_location location = std::source_location::current()) { + try { action(); } catch (const std::exception&) { return; } + Require(false, location); +} + +static std::vector freed; + +static void* APS5_VABI Allocate(std::size_t size) { + return std::malloc(size); +} + +static void APS5_VABI Free(void* pointer) { + freed.push_back(pointer); + std::free(pointer); +} + +static void* APS5_VABI Calloc(std::size_t, std::size_t) { throw std::runtime_error("unexpected calloc"); } +static void* APS5_VABI Reallocate(void*, std::size_t) { throw std::runtime_error("unexpected realloc"); } +static void* APS5_VABI Align(std::size_t, std::size_t) { throw std::runtime_error("unexpected aligned allocation"); } +static void* APS5_VABI Realign(void*, std::size_t, std::size_t) { throw std::runtime_error("unexpected aligned reallocation"); } +static int APS5_VABI PosixAlign(void**, std::size_t, std::size_t) { throw std::runtime_error("unexpected posix_memalign"); } + +static void InitializeHeap() { + void* const api[10] {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); +} + +static void TestNum() { + char first = 'a'; + Require(sk_num_nid_postfix(nullptr) == -1); + LibreSslStack stack {3, nullptr, 0, 0, nullptr}; + char* elements[3] {&first, nullptr, &first}; + stack.data = elements; + Require(sk_num_nid_postfix(&stack) == 3); + stack.num = 0; + Require(sk_num_nid_postfix(&stack) == 0); + stack.num = -2; + Require(sk_num_nid_postfix(&stack) == -2); + LibreSslStack detached {2, nullptr, 0, 0, nullptr}; + Require(sk_num_nid_postfix(&detached) == 2); +} + +static void TestValue() { + char first = 'a'; + char second = 'b'; + char third = 'c'; + Require(sk_value_nid_postfix(nullptr, 0) == nullptr); + char* elements[4] {&first, nullptr, &third, &second}; + LibreSslStack stack {3, elements, 0, 4, nullptr}; + Require(sk_value_nid_postfix(&stack, 0) == &first); + Require(sk_value_nid_postfix(&stack, 1) == nullptr); + Require(sk_value_nid_postfix(&stack, 2) == &third); + Require(sk_value_nid_postfix(&stack, 3) == nullptr); + Require(sk_value_nid_postfix(&stack, -1) == nullptr); + stack.num = -2; + Require(sk_value_nid_postfix(&stack, 0) == nullptr); + LibreSslStack detached {2, nullptr, 0, 0, nullptr}; + Require(sk_value_nid_postfix(&detached, 2) == nullptr); + RequireThrows([&] { sk_value_nid_postfix(&detached, 1); }); +} + +static void TestPop() { + char first = 'a'; + char third = 'c'; + Require(sk_pop_nid_postfix(nullptr) == nullptr); + char* elements[8] {&first, nullptr, &third}; + LibreSslStack stack {3, elements, 1, 8, nullptr}; + Require(sk_pop_nid_postfix(&stack) == &third); + Require(sk_num_nid_postfix(&stack) == 2 && stack.numAlloc == 8 && stack.data == elements && elements[2] == &third && stack.sorted == 1); + Require(sk_value_nid_postfix(&stack, 2) == nullptr); + Require(sk_pop_nid_postfix(&stack) == nullptr && sk_num_nid_postfix(&stack) == 1); + Require(sk_pop_nid_postfix(&stack) == &first && sk_num_nid_postfix(&stack) == 0 && stack.sorted == 1); + Require(sk_pop_nid_postfix(&stack) == nullptr && sk_num_nid_postfix(&stack) == 0); + stack.num = -2; + Require(sk_pop_nid_postfix(&stack) == nullptr && sk_num_nid_postfix(&stack) == -2); + LibreSslStack detached {2, nullptr, 0, 0, nullptr}; + RequireThrows([&] { sk_pop_nid_postfix(&detached); }); + Require(detached.num == 2); +} + +static std::vector released; + +static void APS5_VABI Release(void* element) { + released.push_back(element); +} + +static LibreSslStack* NewStack(std::vector elements, int capacity) { + auto* stack = static_cast(malloc_nid_postfix(sizeof(LibreSslStack))); + Require(stack != nullptr); + std::memset(stack, 0, sizeof(LibreSslStack)); + stack->data = static_cast(malloc_nid_postfix(sizeof(char*) * static_cast(capacity > 0 ? capacity : 1))); + Require(stack->data != nullptr); + for (int index = 0; index < capacity; ++index) stack->data[index] = nullptr; + for (std::size_t index = 0; index < elements.size(); ++index) stack->data[index] = elements[index]; + stack->num = static_cast(elements.size()); + stack->numAlloc = capacity; + return stack; +} + +static void TestPopFree() { + char first = 'a'; + char second = 'b'; + char third = 'c'; + sk_pop_free_nid_postfix(nullptr, Release); + sk_pop_free_nid_postfix(nullptr, nullptr); + Require(released.empty()); + + LibreSslStack* stack = NewStack({&first, nullptr, &second, &third}, 4); + char** const data = stack->data; + freed.clear(); + sk_pop_free_nid_postfix(stack, Release); + Require(released == std::vector({&first, &second, &third})); + Require(freed == std::vector({data, stack})); + released.clear(); + + stack = NewStack({nullptr, nullptr}, 2); + sk_pop_free_nid_postfix(stack, nullptr); + stack = NewStack({}, 0); + sk_pop_free_nid_postfix(stack, nullptr); + stack = NewStack({&first}, 1); + stack->num = -2; + sk_pop_free_nid_postfix(stack, nullptr); + Require(released.empty()); + + stack = NewStack({&first, &second}, 2); + RequireThrows([&] { sk_pop_free_nid_postfix(stack, nullptr); }); + stack->data[0] = nullptr; + stack->data[1] = nullptr; + sk_pop_free_nid_postfix(stack, nullptr); +} + +int main() { + InitializeHeap(); + TestNum(); + TestValue(); + TestPop(); + TestPopFree(); + std::puts("libSceLibreSsl stack tests passed"); +} diff --git a/core/libs/tests/GuestLibreSslUi.cpp b/core/libs/tests/GuestLibreSslUi.cpp new file mode 100644 index 0000000000..0a14b61ba8 --- /dev/null +++ b/core/libs/tests/GuestLibreSslUi.cpp @@ -0,0 +1,148 @@ +#include "prx/libSceLibreSsl/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 + +static int APS5_VABI Open(void*) { return 1; } +static int APS5_VABI Close(void*) { return 2; } +static int APS5_VABI Write(void*, void*) { return 3; } +static int APS5_VABI Read(void*, void*) { return 4; } + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "LibreSSL UI method 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 void InitializeHeap() { + void* const api[10] {}; + ApplicationHeapRegister_nid_no_patch(api); +} + +static void TestCreate() { + char name[] = "title prompt"; + LibreSslUiMethod* method = UI_create_method_nid_postfix(name); + Require(method != nullptr); + Require(method->name != nullptr && method->name != name && !std::strcmp(method->name, "title prompt")); + name[0] = 'X'; + Require(!std::strcmp(method->name, "title prompt")); + Require(!method->openSession && !method->writeString && !method->flush && !method->readString && !method->closeSession && !method->constructPrompt); + free_nid_postfix(method->name); + free_nid_postfix(method); + method = UI_create_method_nid_postfix(nullptr); + Require(method != nullptr && method->name == nullptr); + free_nid_postfix(method); + char empty[] = ""; + method = UI_create_method_nid_postfix(empty); + Require(method != nullptr && method->name != nullptr && method->name != empty && method->name[0] == '\0'); + free_nid_postfix(method->name); + free_nid_postfix(method); +} + +static void TestDestroy() { + char name[] = "prompt"; + UI_destroy_method_nid_postfix(UI_create_method_nid_postfix(name)); + UI_destroy_method_nid_postfix(UI_create_method_nid_postfix(nullptr)); + RequireThrows([] { UI_destroy_method_nid_postfix(nullptr); }); +} + +static void TestSetOpener() { + LibreSslUiMethod* method = UI_create_method_nid_postfix(nullptr); + Require(UI_method_set_opener_nid_postfix(method, Open) == 0 && method->openSession == Open); + Require(!method->writeString && !method->readString && !method->closeSession && !method->flush && !method->constructPrompt); + Require(UI_method_set_opener_nid_postfix(method, nullptr) == 0 && method->openSession == nullptr); + Require(UI_method_set_opener_nid_postfix(nullptr, Open) == -1); + UI_destroy_method_nid_postfix(method); +} + +static void TestGetOpener() { + LibreSslUiMethod* method = UI_create_method_nid_postfix(nullptr); + Require(UI_method_get_opener_nid_postfix(method) == nullptr); + Require(UI_method_set_opener_nid_postfix(method, Open) == 0); + Require(UI_method_get_opener_nid_postfix(method) == Open); + Require(UI_method_get_opener_nid_postfix(method)(nullptr) == 1); + Require(UI_method_get_opener_nid_postfix(nullptr) == nullptr); + UI_destroy_method_nid_postfix(method); +} + +static void TestSetWriter() { + LibreSslUiMethod* method = UI_create_method_nid_postfix(nullptr); + Require(UI_method_set_writer_nid_postfix(method, Write) == 0 && method->writeString == Write); + Require(!method->openSession && !method->readString && !method->closeSession && !method->flush && !method->constructPrompt); + Require(UI_method_set_writer_nid_postfix(method, nullptr) == 0 && method->writeString == nullptr); + Require(UI_method_set_writer_nid_postfix(nullptr, Write) == -1); + UI_destroy_method_nid_postfix(method); +} + +static void TestGetWriter() { + LibreSslUiMethod* method = UI_create_method_nid_postfix(nullptr); + Require(UI_method_get_writer_nid_postfix(method) == nullptr); + Require(UI_method_set_writer_nid_postfix(method, Write) == 0); + Require(UI_method_get_writer_nid_postfix(method) == Write); + Require(UI_method_get_writer_nid_postfix(method)(nullptr, nullptr) == 3); + Require(UI_method_get_writer_nid_postfix(nullptr) == nullptr); + UI_destroy_method_nid_postfix(method); +} + +static void TestSetReader() { + LibreSslUiMethod* method = UI_create_method_nid_postfix(nullptr); + Require(UI_method_set_reader_nid_postfix(method, Read) == 0 && method->readString == Read); + Require(!method->openSession && !method->writeString && !method->closeSession && !method->flush && !method->constructPrompt); + Require(UI_method_set_reader_nid_postfix(method, nullptr) == 0 && method->readString == nullptr); + Require(UI_method_set_reader_nid_postfix(nullptr, Read) == -1); + UI_destroy_method_nid_postfix(method); +} + +static void TestGetReader() { + LibreSslUiMethod* method = UI_create_method_nid_postfix(nullptr); + Require(UI_method_get_reader_nid_postfix(method) == nullptr); + Require(UI_method_set_reader_nid_postfix(method, Read) == 0); + Require(UI_method_get_reader_nid_postfix(method) == Read); + Require(UI_method_get_reader_nid_postfix(method)(nullptr, nullptr) == 4); + Require(UI_method_get_reader_nid_postfix(nullptr) == nullptr); + UI_destroy_method_nid_postfix(method); +} + +static void TestSetCloser() { + LibreSslUiMethod* method = UI_create_method_nid_postfix(nullptr); + Require(UI_method_set_closer_nid_postfix(method, Close) == 0 && method->closeSession == Close); + Require(!method->openSession && !method->writeString && !method->readString && !method->flush && !method->constructPrompt); + Require(UI_method_set_closer_nid_postfix(method, nullptr) == 0 && method->closeSession == nullptr); + Require(UI_method_set_closer_nid_postfix(nullptr, Close) == -1); + UI_destroy_method_nid_postfix(method); +} + +static void TestGetCloser() { + LibreSslUiMethod* method = UI_create_method_nid_postfix(nullptr); + Require(UI_method_get_closer_nid_postfix(method) == nullptr); + Require(UI_method_set_closer_nid_postfix(method, Close) == 0); + Require(UI_method_get_closer_nid_postfix(method) == Close); + Require(UI_method_get_closer_nid_postfix(method)(nullptr) == 2); + Require(UI_method_get_closer_nid_postfix(nullptr) == nullptr); + UI_destroy_method_nid_postfix(method); +} + +int main() { + InitializeHeap(); + TestCreate(); + TestDestroy(); + TestSetOpener(); + TestGetOpener(); + TestSetWriter(); + TestGetWriter(); + TestSetReader(); + TestGetReader(); + TestSetCloser(); + TestGetCloser(); + std::puts("libSceLibreSsl UI method tests passed"); +} diff --git a/core/libs/tests/GuestLibreSslVersion.cpp b/core/libs/tests/GuestLibreSslVersion.cpp new file mode 100644 index 0000000000..b59a170fd2 --- /dev/null +++ b/core/libs/tests/GuestLibreSslVersion.cpp @@ -0,0 +1,18 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "LibreSSL version check failed at line %u\n", location.line()); + std::abort(); +} + +int main() { + Require(SSLeay_nid_postfix() == std::uint64_t{0x20000000}); + Require(SSLeay_nid_postfix() == SSLeay_nid_postfix()); + return 0; +} diff --git a/core/libs/tests/GuestLocale.cpp b/core/libs/tests/GuestLocale.cpp index 3baacd92f3..346286e815 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -22,6 +22,9 @@ extern std::uintptr_t _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIc extern std::uintptr_t _ZTVSt7codecvtIwc9_MbstatetE_nid_postfix[13]; extern std::uintptr_t _ZTVSt7num_putIwSt19ostreambuf_iteratorIwSt11char_traitsIwEEE_nid_postfix[14]; extern std::uintptr_t _ZTVSt7collateIcE_nid_postfix[9]; +extern std::uintptr_t _ZTVSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[13]; +extern std::uintptr_t _ZTVSt8time_getIwSt19istreambuf_iteratorIwSt11char_traitsIwEEE_nid_postfix[13]; +extern std::uintptr_t _ZTVSt7num_getIwSt19istreambuf_iteratorIwSt11char_traitsIwEEE_nid_postfix[17]; } @@ -137,7 +140,7 @@ static void CheckCharacterTables() { Require(classification[-1] == 0 && lower[-1] == -1 && upper[-1] == -1); Require(classification['A'] == 0x03 && classification['a'] == 0x11); Require(classification['G'] == 0x02 && classification['0'] == 0x21); - Require(classification[' '] == 0x04 && classification['\t'] == 0xc0); + Require(classification[' '] == 0x04 && classification['\t'] == 0x4c0); Require(classification['\n'] == 0xc0 && classification['!'] == 0x08); for (int value = 0; value < 256; ++value) { Require(lower[value] == (value >= 'A' && value <= 'Z' ? value + 32 : value)); @@ -334,6 +337,33 @@ static void CheckFacetGetcat() { Require(_ZNSt8numpunctIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&punct, &locale) == 4); } +static void CheckNumGetWideVtableObject() { + for (std::uintptr_t& slot : _ZTVSt7num_getIwSt19istreambuf_iteratorIwSt11char_traitsIwEEE_nid_postfix) { + Require(slot == 0); + slot = 0x1111; + Require(slot == 0x1111); + slot = 0; + } +} + +static void CheckTimeGetVtableObject() { + for (std::uintptr_t& slot : _ZTVSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix) { + Require(slot == 0); + slot = 0x1111; + Require(slot == 0x1111); + slot = 0; + } +} + +static void CheckTimeGetWideVtableObject() { + for (std::uintptr_t& slot : _ZTVSt8time_getIwSt19istreambuf_iteratorIwSt11char_traitsIwEEE_nid_postfix) { + Require(slot == 0); + slot = 0x1111; + Require(slot == 0x1111); + slot = 0; + } +} + int main() { std::array heapApi{}; heapApi[0] = reinterpret_cast(&TestHeapAllocate); @@ -357,6 +387,9 @@ int main() { CheckWideNumPutVtableObject(); CheckFacetGetcat(); CheckCollateVtableObject(); + CheckTimeGetVtableObject(); + CheckTimeGetWideVtableObject(); + CheckNumGetWideVtableObject(); _Locksyslock_nid_postfix(); _Locksyslock_nid_postfix(); _Unlocksyslock_nid_postfix(); diff --git a/core/libs/tests/GuestLoginDialog.cpp b/core/libs/tests/GuestLoginDialog.cpp index 6f919c7cc8..9b567297b0 100644 --- a/core/libs/tests/GuestLoginDialog.cpp +++ b/core/libs/tests/GuestLoginDialog.cpp @@ -26,8 +26,10 @@ int main() { LoginDialogResult result{-1, -1, {11, 22, 33, 44, 55, 66, 77, 88}}; Require(sceLoginDialogTerminate() == kErrNotInitialized); + Require(sceLoginDialogOpen(nullptr) == kErrArgNull); Require(sceLoginDialogOpen(¶m) == kErrNotInitialized); Require(sceLoginDialogClose() == kErrNotInitialized); + Require(sceLoginDialogGetResult(nullptr) == kErrArgNull); Require(sceLoginDialogGetResult(&result) == kErrNotInitialized); Require(sceLoginDialogGetStatus() == kStatusNone); Require(sceLoginDialogUpdateStatus() == kStatusNone); @@ -38,6 +40,7 @@ int main() { Require(sceLoginDialogUpdateStatus() == kStatusInitialized); Require(sceLoginDialogOpen(nullptr) == kErrArgNull); + Require(sceLoginDialogGetResult(nullptr) == kErrArgNull); Require(sceLoginDialogGetResult(&result) == kErrNotFinished); Require(result.result == 1 && result.errorCode == static_cast(0x81340007u)); diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 6a11dd9caa..ea2219eb63 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -542,6 +542,198 @@ static void CheckExpm1ConsoleBits() { } } +static void CheckNextafterConsoleBits() { + constexpr struct { + std::uint64_t x; + std::uint64_t y; + std::uint64_t expected; + int expectedErrno; + } cases[] = { + {0x0000000000000000ull, 0x3ff0000000000000ull, 0x0000000000000001ull, 0}, + {0x8000000000000000ull, 0xbff0000000000000ull, 0x8000000000000001ull, 0}, + {0x0000000000000001ull, 0x0000000000000000ull, 0x0000000000000000ull, ERANGE}, + {0x8000000000000001ull, 0x3ff0000000000000ull, 0x8000000000000000ull, ERANGE}, + {0x0010000000000000ull, 0x0000000000000000ull, 0x000fffffffffffffull, ERANGE}, + {0x000fffffffffffffull, 0x3ff0000000000000ull, 0x0010000000000000ull, 0}, + {0x7fefffffffffffffull, 0x7ff0000000000000ull, 0x7ff0000000000000ull, ERANGE}, + {0xffefffffffffffffull, 0xfff0000000000000ull, 0xfff0000000000000ull, ERANGE}, + {0x7ff0000000000000ull, 0x0000000000000000ull, 0x7fefffffffffffffull, 0}, + {0x3ff0000000000000ull, 0x4000000000000000ull, 0x3ff0000000000001ull, 0}, + {0x3ff0000000000000ull, 0x3ff0000000000000ull, 0x3ff0000000000000ull, 0}, + {0x8000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0}, + {0x7ff0000000000001ull, 0x3ff0000000000000ull, 0x7ff8000000000001ull, 0}, + {0x3ff0000000000000ull, 0xfff8000000000123ull, 0xfff8000000000123ull, 0}, + {0x7ff8000000000123ull, 0xfff8000000000456ull, 0x7ff8000000000123ull, 0}, + {0xfff8000000000456ull, 0x7ff0000000000001ull, 0xfff8000000000456ull, 0}, + }; + for (const auto& item : cases) { + errno = 1234; + Require(std::bit_cast(nextafter_nid_postfix(std::bit_cast(item.x), std::bit_cast(item.y))) == item.expected); + Require(errno == (item.expectedErrno != 0 ? item.expectedErrno : 1234)); + } + constexpr struct { + std::uint32_t x; + std::uint32_t y; + std::uint32_t expected; + int expectedErrno; + } floatCases[] = { + {0x00000000u, 0x3f800000u, 0x00000001u, 0}, + {0x80000000u, 0xbf800000u, 0x80000001u, 0}, + {0x00000001u, 0x00000000u, 0x00000000u, ERANGE}, + {0x80000001u, 0x3f800000u, 0x80000000u, ERANGE}, + {0x00800000u, 0x00000000u, 0x007fffffu, ERANGE}, + {0x007fffffu, 0x3f800000u, 0x00800000u, 0}, + {0x7f7fffffu, 0x7f800000u, 0x7f800000u, ERANGE}, + {0xff7fffffu, 0xff800000u, 0xff800000u, ERANGE}, + {0x7f800000u, 0x00000000u, 0x7f7fffffu, 0}, + {0x3f800000u, 0x40000000u, 0x3f800001u, 0}, + {0x3f800000u, 0x3f800000u, 0x3f800000u, 0}, + {0x80000000u, 0x00000000u, 0x00000000u, 0}, + {0x7f800001u, 0x3f800000u, 0x7fc00001u, 0}, + {0x3f800000u, 0xffc00123u, 0xffc00123u, 0}, + {0x7fc00123u, 0xffc00456u, 0x7fc00123u, 0}, + {0xffc00456u, 0x7f800001u, 0xffc00456u, 0}, + }; + for (const auto& item : floatCases) { + errno = 1234; + Require(std::bit_cast(nextafterf_nid_postfix(std::bit_cast(item.x), std::bit_cast(item.y))) == item.expected); + Require(errno == (item.expectedErrno != 0 ? item.expectedErrno : 1234)); + } +} + +static void CheckLog1pConsoleBits() { + constexpr struct { + std::uint64_t input; + std::uint64_t expected; + int error; + } cases[] = { + {0x0000000000000000ull, 0x0000000000000000ull, 0}, {0x8000000000000000ull, 0x8000000000000000ull, 0}, + {0x3c8fffffffffffffull, 0x3c8fffffffffffffull, 0}, {0x3c90000000000000ull, 0x3c90000000000000ull, 0}, + {0x3e1fffffffffffffull, 0x3e1fffffff7fffffull, 0}, {0x3e20000000000000ull, 0x3e1fffffff800000ull, 0}, + {0xbe20000000000000ull, 0xbe20000000400000ull, 0}, {0x3fda8279ffffffffull, 0x3fd62e43471c5d9dull, 0}, + {0x3fda827a00000000ull, 0x3fd62e43471c5d9eull, 0}, {0xbfd2bec400000000ull, 0xbfd62e4420e1ff10ull, 0}, + {0xbfd2bec3ffffffffull, 0xbfd62e4420e1ff0full, 0}, {0x3ff0000000000000ull, 0x3fe62e42fefa39efull, 0}, + {0xbfe0000000000000ull, 0xbfe62e42fefa39efull, 0}, {0xbfefffffffffffffull, 0xc0425e4f7b2737faull, 0}, + {0xbff0000000000000ull, 0xfff0000000000000ull, 0}, {0xbff0000000000001ull, 0xfff8000000000000ull, EDOM}, + {0xc000000000000000ull, 0xfff8000000000000ull, EDOM}, {0x433fffffffffffffull, 0x40425e4f7b2737faull, 0}, + {0x4340000000000000ull, 0x40425e4f7b2737faull, 0}, {0x7fefffffffffffffull, 0x40862e42fefa39efull, 0}, + {0x7ff0000000000000ull, 0x7ff0000000000000ull, 0}, {0xfff0000000000000ull, 0xfff8000000000000ull, EDOM}, + {0x7ff8000000000001ull, 0x7ff8000000000001ull, 0}, {0xfff8000000000002ull, 0xfff8000000000002ull, EDOM}, + {0x3f0f5ab6d3865ccaull, 0x3f0f5a7962a0216cull, 0}, {0x3fe8683f0ace287cull, 0x3fe223b9ede79242ull, 0}, + {0x3f5f8cd247124706ull, 0x3f5f850e039f6187ull, 0}, {0x40298faede87074bull, 0x4004fc71ddb00cc0ull, 0}, + {0x404b621796f08648ull, 0x401015adbb851c70ull, 0}, {0x3f21ecf9339d7420ull, 0x3f21eca8e0601790ull, 0}, + }; + for (const auto& item : cases) { + errno = 1234; + Require(std::bit_cast(log1p_nid_postfix(std::bit_cast(item.input))) == item.expected); + Require(errno == (item.error != 0 ? item.error : 1234)); + } +} + +static void CheckTanhConsoleBits() { + constexpr std::uint64_t cases[][2] = { + {0x0000000000000000ull, 0x0000000000000000ull}, {0x8000000000000000ull, 0x8000000000000000ull}, + {0x3e2fffffffffffffull, 0x3e2fffffffffffffull}, {0x3e30000000000000ull, 0x3e30000000000000ull}, + {0xbe30000000000000ull, 0xbe30000000000000ull}, {0x3fefffffffffffffull, 0x3fe85efab514f394ull}, + {0x3ff0000000000000ull, 0x3fe85efab514f394ull}, {0xbff0000000000000ull, 0xbfe85efab514f394ull}, + {0x3fe0000000000000ull, 0x3fdd9353d7568af3ull}, {0x4035ffffffffffffull, 0x3ff0000000000000ull}, + {0x4036000000000000ull, 0x3ff0000000000000ull}, {0xc036000000000000ull, 0xbff0000000000000ull}, + {0x4049000000000000ull, 0x3ff0000000000000ull}, {0x7fefffffffffffffull, 0x3ff0000000000000ull}, + {0x7ff0000000000000ull, 0x3ff0000000000000ull}, {0xfff0000000000000ull, 0xbff0000000000000ull}, + {0x7ff8000000000001ull, 0x7ff8000000000001ull}, {0xfff8000000000002ull, 0xfff8000000000002ull}, + {0x3f927ed0db8370b1ull, 0x3f927e4d1092a580ull}, {0x3f95d74e4cee78bfull, 0x3f95d67547d523a0ull}, + {0x3f18f1b062cb523cull, 0x3f18f1b06187faaeull}, {0x3f937129f25ae835ull, 0x3f937090dd8c9cb3ull}, + {0x3facb0d89f8aa91cull, 0x3faca92afe31a32bull}, {0x3f07f462f020acdeull, 0x3f07f462efd91530ull}, + }; + for (const auto& [input, expected] : cases) { + errno = 1234; + Require(std::bit_cast(tanh_nid_postfix(std::bit_cast(input))) == expected && errno == 1234); + } +} + +static void CheckRoundingNaNConsoleBits() { + constexpr std::uint64_t cases[][2] = { + {0x7ff0000000000001ull, 0x7ff8000000000001ull}, {0xfff4000000000123ull, 0xfffc000000000123ull}, {0x7ff8000000000456ull, 0x7ff8000000000456ull}, + {0x7ff0000000000000ull, 0x7ff0000000000000ull}, {0xfff0000000000000ull, 0xfff0000000000000ull}, + }; + constexpr std::uint32_t floatCases[][2] = { + {0x7f800001u, 0x7fc00001u}, {0xffa00123u, 0xffe00123u}, {0x7fc00456u, 0x7fc00456u}, {0x7f800000u, 0x7f800000u}, {0xff800000u, 0xff800000u}, + }; + for (const auto function : {trunc_nid_postfix, ceil_nid_postfix, floor_nid_postfix, nearbyint_nid_postfix}) { + for (const auto& [input, expected] : cases) { + Require(std::bit_cast(function(std::bit_cast(input))) == expected); + } + } + for (const auto function : {truncf_nid_postfix, ceilf_nid_postfix, floorf_nid_postfix, nearbyintf_nid_postfix}) { + for (const auto& [input, expected] : floatCases) { + Require(std::bit_cast(function(std::bit_cast(input))) == expected); + } + } +} + +template +static void CheckNextafterEnvironment(T (APS5_VABI *next)(T, T), UInt sign, UInt one, UInt minimum, UInt maximum, UInt infinity) { + const RestoreFloatingPointState saved; + const auto savedControl = _mm_getcsr(); + const struct { + UInt x, y, normalResult, dazResult; + unsigned normalFlags, dazFlags; + int normalError, dazError; + } cases[] = { + {0, one, 1, 0, 0x32, 0, 97, 97}, + {sign, sign | one, sign | 1, 0, 0x32, 0, 97, 97}, + {1, 0, 0, 0, 0x32, 0, ERANGE, 97}, + {1, one, 2, 2, 0x32, 0, ERANGE, 97}, + {sign | 1, sign | one, sign | 2, sign | 2, 0x32, 0, ERANGE, 97}, + {minimum, 0, minimum - 1, minimum - 1, 0x30, 0x30, ERANGE, ERANGE}, + {maximum, infinity, infinity, infinity, 0x28, 0x28, ERANGE, ERANGE}, + {1, 1, 1, 1, 0x02, 0, 97, 97}, + {one, one + 1, one + 1, one + 1, 0, 0, 97, 97}, + }; + for (unsigned denorm = 0; denorm != 4; ++denorm) { + const unsigned control = 0x1f80u | ((denorm & 1u) ? 0x40u : 0u) | ((denorm & 2u) ? 0x8000u : 0u); + const bool daz = (denorm & 1u) != 0; + for (const auto& item : cases) { + Require(std::fesetround(FE_TONEAREST) == 0); + Require(std::feclearexcept(FE_ALL_EXCEPT) == 0); + errno = 97; + _mm_setcsr(control); + const auto result = std::bit_cast(next(std::bit_cast(item.x), std::bit_cast(item.y))); + const auto flags = _mm_getcsr() & 0x3fu; + const int exceptions = std::fetestexcept(FE_ALL_EXCEPT); + const int error = errno; + const auto expectedFlags = daz ? item.dazFlags : item.normalFlags; + Require(result == (daz ? item.dazResult : item.normalResult)); + Require(flags == expectedFlags); + const int expectedExceptions = ((expectedFlags & 0x01u) ? FE_INVALID : 0) | + ((expectedFlags & 0x04u) ? FE_DIVBYZERO : 0) | + ((expectedFlags & 0x08u) ? FE_OVERFLOW : 0) | + ((expectedFlags & 0x10u) ? FE_UNDERFLOW : 0) | + ((expectedFlags & 0x20u) ? FE_INEXACT : 0); + Require(exceptions == expectedExceptions); + Require(error == (daz ? item.dazError : item.normalError)); + } + } + constexpr int rounding[] = {FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}; + for (const int mode : rounding) { + Require(std::fesetround(mode) == 0); + const unsigned control = (_mm_getcsr() & 0x6000u) | 0x1f80u; + errno = 97; + Require(std::feclearexcept(FE_ALL_EXCEPT) == 0); + _mm_setcsr(control); + Require(std::bit_cast(next(std::bit_cast(maximum), std::bit_cast(infinity))) == (mode == FE_UPWARD ? infinity : maximum)); + Require((_mm_getcsr() & 0x3fu) == 0x28u); + Require(errno == ERANGE); + errno = 97; + Require(std::feclearexcept(FE_ALL_EXCEPT) == 0); + _mm_setcsr(control); + Require(std::bit_cast(next(std::bit_cast(UInt{1}), std::bit_cast(one))) == 2); + Require((_mm_getcsr() & 0x3fu) == 0x32u); + Require(errno == (mode == FE_UPWARD ? 97 : ERANGE)); + } + _mm_setcsr(savedControl); +} + static void CheckAsinl() { const struct { std::uint64_t inputMantissa; std::uint16_t inputSignExponent; std::uint64_t outputMantissa; std::uint16_t outputSignExponent; } cases[] = { {0, 0x0000, 0, 0x0000}, {0, 0x8000, 0, 0x8000}, @@ -845,6 +1037,26 @@ static void CheckExtendedMath() { Require(std::isnan(remainder_nid_postfix(1.0, 0.0)) && std::isnan(remainder_nid_postfix(INFINITY, 1.0))); } +static void CheckExp2fConsoleBits() { + constexpr struct { + std::uint32_t input; + std::uint32_t expected; + int error; + } cases[] = { + {0x3d00010bu, 0x3f82cd8du, 0}, {0x3f000303u, 0x3fb5066du, 0}, {0x40a0051bu, 0x42000e29u, 0}, {0x42f0070cu, 0x7b813a1bu, 0}, + {0xbf400909u, 0x3f183437u, 0}, {0xc2f00b14u, 0x037c3073u, 0}, {0x00000000u, 0x3f800000u, 0}, {0x80000000u, 0x3f800000u, 0}, + {0x33000000u, 0x3f800000u, 0}, {0x3f800000u, 0x40000000u, 0}, {0x42fe0000u, 0x7f000000u, 0}, {0x42ffffffu, 0x7f7fffa7u, 0}, + {0x43000000u, 0x7f800000u, ERANGE}, {0x7f7fffffu, 0x7f800000u, ERANGE}, {0x7f800000u, 0x7f800000u, 0}, {0xff800000u, 0x00000000u, 0}, + {0xc2fc0000u, 0x00800000u, 0}, {0xc3150000u, 0x00000001u, 0}, {0xc315ffffu, 0x00000001u, 0}, {0xc3160000u, 0x00000000u, ERANGE}, + {0xc3160001u, 0x00000000u, ERANGE}, {0xff7fffffu, 0x00000000u, ERANGE}, {0x7fc00001u, 0x7fc00001u, 0}, {0xffc00002u, 0xffc00002u, 0}, + }; + for (const auto& item : cases) { + errno = 1234; + Require(std::bit_cast(exp2f_nid_postfix(std::bit_cast(item.input))) == item.expected); + Require(errno == (item.error != 0 ? item.error : 1234)); + } +} + static void CheckHyperbolic() { const double infinity = std::numeric_limits::infinity(); Require(_Cosh_nid_postfix(0.0, 2.0) == 2.0); @@ -875,11 +1087,62 @@ static void CheckHyperbolic() { Require(Near(acosh_nid_postfix(1e300), 691.4686750787736, 1e-15)); } +static void CheckAcoshfDomainEnvironment() { + const RestoreFloatingPointState saved; + const unsigned savedCsr = _mm_getcsr(); + const struct { + std::uint32_t input; + std::uint32_t result; + unsigned raised; + unsigned x87Raised; + int error; + } cases[] = { + {0x00000000u, 0xffc00000u, 1u, 1u, EDOM}, + {0x80000000u, 0xffc00000u, 1u, 1u, EDOM}, + {0x3f000000u, 0xffc00000u, 1u, 1u, EDOM}, + {0x3f7fffffu, 0xffc00000u, 1u, 1u, EDOM}, + {0xbf800000u, 0xffc00000u, 1u, 1u, EDOM}, + {0xff800000u, 0xffc00000u, 1u, 1u, EDOM}, + {0xffc00001u, 0xffc00001u, 1u, 1u, EDOM}, + {0xff800001u, 0xffc00001u, 1u, 1u, EDOM}, + {0x3f800000u, 0x00000000u, 0u, 0u, 1234}, + {0x7f800000u, 0x7f800000u, 0u, 0u, 1234}, + {0x7fc00001u, 0x7fc00001u, 0u, 0u, 1234}, + {0x7f800001u, 0x7fc00001u, 1u, 0u, 1234}, + }; + const int rounding[] = {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}; + float (APS5_VABI * volatile call)(float) = acoshf_nid_postfix; + for (unsigned mode = 0; mode != 4; ++mode) { + for (unsigned denorm = 0; denorm != 4; ++denorm) { + const unsigned control = 0x1f80u | (mode << 13u) | ((denorm & 1u) ? 0x40u : 0u) | ((denorm & 2u) ? 0x8000u : 0u); + for (std::uint16_t sticky : {0u, 5u, 32u}) { + for (const auto& item : cases) { + Require(std::fesetenv(FE_DFL_ENV) == 0 && std::fesetround(rounding[mode]) == 0); + Require(fesetexceptflag_nid_postfix(&sticky, 0x3f) == 0); + _mm_setcsr(control | sticky); + GuestFloatingPointEnvironment before; + Require(fegetenv_nid_postfix(&before) == 0 && (before.status & 0x3fu) == sticky); + errno = 1234; + const auto result = std::bit_cast(call(std::bit_cast(item.input))); + const int error = errno; + GuestFloatingPointEnvironment after; + Require(fegetenv_nid_postfix(&after) == 0); + if (result != item.result || after.mxcsr != (control | sticky | item.raised) || (after.status & 0x3fu) != (sticky | item.x87Raised) || after.control != before.control || error != item.error) { + std::fprintf(stderr, "acoshf domain: input=%08llx mode=%u denorm=%u sticky=%u bits=%08llx csr=%x x87=%x errno=%d\n", static_cast(item.input), mode, denorm, sticky, static_cast(result), after.mxcsr, after.status, error); + Require(false); + } + } + } + } + } + _mm_setcsr(savedCsr); +} + static void CheckAcoshfMsunBoundaries() { Require(acoshf_nid_postfix(1.0f) == 0.0f && !std::signbit(acoshf_nid_postfix(1.0f))); errno = EINTR; std::feclearexcept(FE_ALL_EXCEPT); - Require(std::isnan(acoshf_nid_postfix(0.5f)) && errno == EINTR && (std::fetestexcept(FE_INVALID) & FE_INVALID) != 0); + Require(std::isnan(acoshf_nid_postfix(0.5f)) && errno == EDOM && (std::fetestexcept(FE_INVALID) & FE_INVALID) != 0); for (const std::uint32_t bits : {0x3f800001u, 0x3fffffffu, 0x40000000u, 0x40000001u, 0x4d7fffffu, 0x4d800000u, 0x4d800001u, 0x7f7fffffu}) { const float x = std::bit_cast(bits); @@ -888,6 +1151,53 @@ static void CheckAcoshfMsunBoundaries() { Require(std::isinf(acoshf_nid_postfix(INFINITY)) && acoshf_nid_postfix(INFINITY) > 0.0f); Require(std::isnan(acoshf_nid_postfix(-INFINITY))); Require(std::isnan(acoshf_nid_postfix(std::numeric_limits::quiet_NaN()))); + const RestoreFloatingPointState saved; + const unsigned savedCsr = _mm_getcsr(); + const struct { + std::uint32_t input; + std::uint32_t result[4]; + unsigned raised; + } cases[] = { + {0x3f800000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, 0u}, + {0x3f800001u, {0x39ffffffu, 0x39ffffffu, 0x3a000001u, 0x39ffffffu}, 32u}, + {0x3fffffffu, {0x3fa89213u, 0x3fa89212u, 0x3fa89214u, 0x3fa89212u}, 32u}, + {0x40000000u, {0x3fa89214u, 0x3fa89213u, 0x3fa89214u, 0x3fa89213u}, 32u}, + {0x40000001u, {0x3fa89215u, 0x3fa89214u, 0x3fa89215u, 0x3fa89214u}, 32u}, + {0x40000002u, {0x3fa89216u, 0x3fa89215u, 0x3fa89217u, 0x3fa89215u}, 32u}, + {0x40000003u, {0x3fa89217u, 0x3fa89217u, 0x3fa89218u, 0x3fa89217u}, 32u}, + {0x4d7fffffu, {0x41a0cf66u, 0x41a0cf65u, 0x41a0cf66u, 0x41a0cf65u}, 32u}, + {0x4d800000u, {0x41a0cf66u, 0x41a0cf64u, 0x41a0cf66u, 0x41a0cf64u}, 32u}, + {0x4d800001u, {0x41a0cf66u, 0x41a0cf65u, 0x41a0cf67u, 0x41a0cf65u}, 32u}, + {0x4d8050f2u, {0x41a0d473u, 0x41a0d472u, 0x41a0d474u, 0x41a0d472u}, 32u}, + {0x7f7fffffu, {0x42b2d4fcu, 0x42b2d4fbu, 0x42b2d4fdu, 0x42b2d4fbu}, 32u}, + }; + const int rounding[] = {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}; + float (APS5_VABI * volatile call)(float) = acoshf_nid_postfix; + for (unsigned mode = 0; mode != 4; ++mode) { + for (unsigned denorm = 0; denorm != 4; ++denorm) { + const unsigned control = 0x1f80u | (mode << 13u) | ((denorm & 1u) ? 0x40u : 0u) | ((denorm & 2u) ? 0x8000u : 0u); + for (std::uint16_t sticky : {0u, 5u, 32u}) { + for (const auto& item : cases) { + Require(std::fesetenv(FE_DFL_ENV) == 0 && std::fesetround(rounding[mode]) == 0); + Require(fesetexceptflag_nid_postfix(&sticky, 0x3f) == 0); + _mm_setcsr(control | sticky); + GuestFloatingPointEnvironment before; + Require(fegetenv_nid_postfix(&before) == 0 && (before.status & 0x3fu) == sticky); + errno = 1234; + const auto result = std::bit_cast(call(std::bit_cast(item.input))); + const int error = errno; + GuestFloatingPointEnvironment after; + Require(fegetenv_nid_postfix(&after) == 0); + if (result != item.result[mode] || after.mxcsr != (control | sticky | item.raised) || (after.status & 0x3fu) != sticky || after.control != before.control || error != 1234) { + std::fprintf(stderr, "acoshf positive: input=%08llx mode=%u denorm=%u sticky=%u bits=%08llx expected=%08llx csr=%x x87=%x errno=%d\n", static_cast(item.input), mode, denorm, sticky, static_cast(result), static_cast(item.result[mode]), after.mxcsr, after.status, error); + Require(false); + } + } + } + } + } + _mm_setcsr(savedCsr); + } static void CheckCoshfMsunBoundaries() { @@ -1219,6 +1529,53 @@ static bool Near(long double actual, long double expected) { return std::fabs(actual - expected) <= std::fabs(expected) * 1e-17L; } +static void CheckAcosfMsun() { + Require(std::bit_cast(acosf_nid_postfix(std::bit_cast(0x3d58d031u))) == 0x3fc2488au); + Require(std::bit_cast(acosf_nid_postfix(std::bit_cast(0x3f00b99au))) == 0x3f859f4fu); + Require(std::bit_cast(acosf_nid_postfix(std::bit_cast(0xbf00b99au))) == 0x40064033u); + Require(std::bit_cast(acosf_nid_postfix(1.0f)) == 0u); + Require(std::bit_cast(acosf_nid_postfix(-1.0f)) == 0x40490fdbu); + Require(std::bit_cast(acosf_nid_postfix(std::bit_cast(0x32800000u))) == 0x3fc90fdbu); +} + +static void CheckLongDoubleHyperbolic() { + const long double infinity = std::numeric_limits::infinity(); + const long double nan = std::numeric_limits::quiet_NaN(); + Require(_LCosh_nid_postfix(0.0L, 2.0L) == 2.0L); + Require(Near(_LCosh_nid_postfix(1.0L, 2.0L), 3.08616126963048755696L)); + Require(Near(_LCosh_nid_postfix(-1.0L, -2.0L), -3.08616126963048755696L)); + Require(std::isinf(_LCosh_nid_postfix(infinity, -1.0L)) && std::signbit(_LCosh_nid_postfix(infinity, -1.0L))); + Require(std::isnan(_LCosh_nid_postfix(infinity, 0.0L))); + Require(std::isnan(_LCosh_nid_postfix(nan, 1.0L))); + Require(_LCosh_nid_postfix(720.0L, 0.0L) == 0.0L && !std::signbit(_LCosh_nid_postfix(720.0L, 0.0L))); + Require(_LCosh_nid_postfix(720.0L, -0.0L) == 0.0L && std::signbit(_LCosh_nid_postfix(720.0L, -0.0L))); + Require(Near(_LCosh_nid_postfix(11357.0L, 0x1p-16000L), 0xc.e2773666cc8cc68p+380L)); + Require(Near(_LCosh_nid_postfix(-11357.0L, 0x1p-16000L), 0xc.e2773666cc8cc68p+380L)); + Require(Near(_LCosh_nid_postfix(11360.0L, 0x1p-16384L), 0x8.165ba1f5c0ff231p+1L)); + Require(std::isinf(_LCosh_nid_postfix(11360.0L, 1.0L))); + Require(_LSinh_nid_postfix(0.0L, 2.0L) == 0.0L && !std::signbit(_LSinh_nid_postfix(0.0L, 2.0L))); + Require(_LSinh_nid_postfix(0.0L, -2.0L) == 0.0L && std::signbit(_LSinh_nid_postfix(0.0L, -2.0L))); + Require(Near(_LSinh_nid_postfix(1.0L, 2.0L), 2.35040238728760291376L)); + Require(Near(_LSinh_nid_postfix(-1.0L, -2.0L), 2.35040238728760291376L)); + Require(Near(_LSinh_nid_postfix(1.0L, -2.0L), -2.35040238728760291376L)); + Require(std::isinf(_LSinh_nid_postfix(infinity, -1.0L)) && std::signbit(_LSinh_nid_postfix(infinity, -1.0L))); + Require(std::isinf(_LSinh_nid_postfix(-infinity, -1.0L)) && !std::signbit(_LSinh_nid_postfix(-infinity, -1.0L))); + Require(std::isnan(_LSinh_nid_postfix(infinity, 0.0L))); + Require(std::isnan(_LSinh_nid_postfix(nan, 1.0L))); + Require(_LSinh_nid_postfix(720.0L, 0.0L) == 0.0L && !std::signbit(_LSinh_nid_postfix(720.0L, 0.0L))); + Require(_LSinh_nid_postfix(720.0L, -0.0L) == 0.0L && std::signbit(_LSinh_nid_postfix(720.0L, -0.0L))); + Require(_LSinh_nid_postfix(-720.0L, 0.0L) == 0.0L && std::signbit(_LSinh_nid_postfix(-720.0L, 0.0L))); + Require(_LSinh_nid_postfix(-720.0L, -0.0L) == 0.0L && !std::signbit(_LSinh_nid_postfix(-720.0L, -0.0L))); + Require(Near(_LSinh_nid_postfix(11357.0L, 0x1p-16000L), 0xc.e2773666cc8cc68p+380L)); + Require(Near(_LSinh_nid_postfix(-11357.0L, 0x1p-16000L), -0xc.e2773666cc8cc68p+380L)); + Require(Near(_LSinh_nid_postfix(11357.0L, -0x1p-16000L), -0xc.e2773666cc8cc68p+380L)); + Require(Near(_LSinh_nid_postfix(-11357.0L, -0x1p-16000L), 0xc.e2773666cc8cc68p+380L)); + Require(Near(_LSinh_nid_postfix(11360.0L, 0x1p-16384L), 0x8.165ba1f5c0ff231p+1L)); + Require(Near(_LSinh_nid_postfix(-11360.0L, 0x1p-16384L), -0x8.165ba1f5c0ff231p+1L)); + Require(std::isinf(_LSinh_nid_postfix(11360.0L, 1.0L))); + Require(std::isinf(_LSinh_nid_postfix(-11360.0L, 1.0L)) && std::signbit(_LSinh_nid_postfix(-11360.0L, 1.0L))); +} + static void CheckLongDoubleFunctions() { const long double infinity = std::numeric_limits::infinity(); const long double nan = std::numeric_limits::quiet_NaN(); @@ -1556,6 +1913,12 @@ int main() { CheckLongDouble(); CheckNearbyintl(); CheckExpm1ConsoleBits(); + CheckLog1pConsoleBits(); + CheckNextafterConsoleBits(); + CheckTanhConsoleBits(); + CheckRoundingNaNConsoleBits(); + 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}); CheckAsinl(); Require(scalbn_nid_postfix(0.75, -2) == 0.1875); Require(scalbnf_nid_postfix(0.75f, 4) == 12.f); @@ -1643,6 +2006,7 @@ int main() { CheckPowfSpecialValue(); Require(abs_nid_postfix(0) == 0); CheckAcosfErrno(); + CheckAcosfMsun(); Require(abs_nid_postfix(42) == 42); Require(abs_nid_postfix(-42) == 42); Require(abs_nid_postfix(std::numeric_limits::max()) == std::numeric_limits::max()); @@ -1678,8 +2042,10 @@ int main() { Require(std::isinf(__powidf2_nid_postfix(-0.0, -3)) && std::signbit(__powidf2_nid_postfix(-0.0, -3))); Require(__powidf2_nid_postfix(2.0, -127) == 0x1p-127); CheckHyperbolic(); + CheckAcoshfDomainEnvironment(); CheckAcoshfMsunBoundaries(); CheckCoshfMsunBoundaries(); + CheckExp2fConsoleBits(); CheckScaledHyperbolic(); CheckSinhf(); CheckAtanhf(); @@ -1693,5 +2059,6 @@ int main() { CheckIntegerAbsAndDivision(); CheckScaling(); CheckLongDoubleFunctions(); + CheckLongDoubleHyperbolic(); CheckDinkumwareFloatCharacteristics(); } diff --git a/core/libs/tests/GuestMathExact.cpp b/core/libs/tests/GuestMathExact.cpp index d9898fd653..5db64f610d 100644 --- a/core/libs/tests/GuestMathExact.cpp +++ b/core/libs/tests/GuestMathExact.cpp @@ -10,7 +10,7 @@ static void Require(bool value) { if (!value) std::abort(); } -static long double Extended(std::uint16_t signExponent, std::uint64_t mantissa) { +[[gnu::noinline]] static long double Extended(std::uint16_t signExponent, std::uint64_t mantissa) { long double value = 0; unsigned char bytes[sizeof(long double)] = {}; std::memcpy(bytes, &mantissa, sizeof(mantissa)); @@ -21,6 +21,40 @@ static long double Extended(std::uint16_t signExponent, std::uint64_t mantissa) static bool SameBits(long double left, long double right) { return std::memcmp(&left, &right, 10) == 0; } +static void CheckRoundingAndScaling() { + Require(copysignl_nid_postfix(3.0L, -1.0L) == -3.0L && copysignl_nid_postfix(-3.0L, 0.0L) == 3.0L); + Require(std::signbit(copysignl_nid_postfix(0.0L, -0.0L)) && !std::signbit(copysignl_nid_postfix(-0.0L, 2.0L))); + Require(sqrtl_nid_postfix(6.25L) == 2.5L && std::isnan(sqrtl_nid_postfix(-1.0L))); + Require(sqrtl_nid_postfix(-0.0L) == 0.0L && std::signbit(sqrtl_nid_postfix(-0.0L))); + + const int saved = std::fegetround(); + const struct { + int mode; + long long half; + long long negativeHalf; + } modes[] = {{FE_TONEAREST, 2, -2}, {FE_UPWARD, 3, -2}, {FE_DOWNWARD, 2, -3}, {FE_TOWARDZERO, 2, -2}}; + for (const auto& mode : modes) { + Require(std::fesetround(mode.mode) == 0); + errno = 0; + Require(lrintl_nid_postfix(2.5L) == mode.half && lrintl_nid_postfix(-2.5L) == mode.negativeHalf); + Require(llrintl_nid_postfix(2.5L) == mode.half && llrintl_nid_postfix(-2.5L) == mode.negativeHalf); + Require(lrintf_nid_postfix(2.5f) == mode.half && lrintf_nid_postfix(-2.5f) == mode.negativeHalf); + Require(llrintf_nid_postfix(2.5f) == mode.half && llrintf_nid_postfix(-2.5f) == mode.negativeHalf); + Require(errno == 0); + } + std::fesetround(saved); + Require(llrintl_nid_postfix(3.5L) == 4 && lrintf_nid_postfix(16777216.0f) == 16777216); + + Require(scalbf_nid_postfix(1.5f, 3.0f) == 12.0f && scalbf_nid_postfix(12.0f, -2.0f) == 3.0f); + Require(std::isnan(scalbf_nid_postfix(1.0f, 0.5f)) && std::isnan(scalbf_nid_postfix(NAN, 1.0f))); + Require(std::isinf(scalbf_nid_postfix(1.0f, INFINITY)) && scalbf_nid_postfix(8.0f, -INFINITY) == 0.0f); + Require(scalblnf_nid_postfix(1.0f, 10) == 1024.0f && scalblnf_nid_postfix(1024.0f, -10) == 1.0f); + Require(std::isinf(scalblnf_nid_postfix(1.0f, INT64_MAX)) && scalblnf_nid_postfix(1.0f, INT64_MIN) == 0.0f); + + Require(__isinfl_nid_postfix(static_cast(INFINITY)) == 1 && __isinfl_nid_postfix(-static_cast(INFINITY)) == 1); + Require(__isinfl_nid_postfix(1.0L) == 0 && __isinfl_nid_postfix(static_cast(NAN)) == 0 && __isinfl_nid_postfix(0.0L) == 0); +} + static void CheckSignalingNanPassThrough() { const long double signaling = Extended(0x7fff, 0x8000000000001234ull); Require(SameBits(fdiml_nid_postfix(signaling, 1.0L), signaling)); @@ -106,6 +140,7 @@ static void CheckClassification() { } int main() { + CheckRoundingAndScaling(); CheckSignalingNanPassThrough(); CheckErrno(); CheckConsoleQuirks(); diff --git a/core/libs/tests/GuestMbus.cpp b/core/libs/tests/GuestMbus.cpp index 4050f1d452..29ef46dc7e 100644 --- a/core/libs/tests/GuestMbus.cpp +++ b/core/libs/tests/GuestMbus.cpp @@ -26,6 +26,11 @@ int main() { param.reserved = 1; Require(sceDeviceServiceInitialize(3, ¶m) == invalidArgument, "reserved field accepted"); param.reserved = 0; + DeviceServiceThreadParam threadParam{}; + threadParam.reserved = 1; + param.threadParam = &threadParam; + Require(sceDeviceServiceInitialize(3, ¶m) == invalidArgument, "threadParam reserved field accepted"); + param.threadParam = nullptr; Require(sceDeviceServiceInitialize(3, ¶m) == 0, "default parameter initialization failed"); Require(sceDeviceServiceInitialize(3, 0) == 0, "initialization failed"); Require(sceDeviceServiceGetGeneration() == 0, "the device set started at another generation"); diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 7f78313a05..f31f21accc 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -1776,7 +1776,7 @@ static void CheckSingleDirectMappingTracksWritesWithoutFaults() { #endif } -static void CheckLargeResidentWritesStopFaulting() { +static void CheckLargeResidentWritesDoNotFault() { #ifdef _WIN32 constexpr std::size_t bytes = 0x200000; std::int64_t physical = 0; @@ -1792,7 +1792,7 @@ static void CheckLargeResidentWritesStopFaulting() { Require(collect() == bytes / 4096); Require(collect() == 0 && collect() == 0); MEMORY_BASIC_INFORMATION memory{}; - Require(VirtualQuery(mapped, &memory, sizeof(memory)) == sizeof(memory) && memory.Protect == PAGE_READONLY); + Require(VirtualQuery(mapped, &memory, sizeof(memory)) == sizeof(memory) && memory.Protect == PAGE_READWRITE); auto* data = static_cast(mapped); const auto path = std::filesystem::path("anyps5-resident-read-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".bin"); @@ -1820,7 +1820,7 @@ static void CheckLargeResidentWritesStopFaulting() { Require(collect() == 1 && collect() == 0); } for (unsigned int round = 0; round < 16; ++round) Require(collect() == 0); - Require(VirtualQuery(mapped, &memory, sizeof(memory)) == sizeof(memory) && memory.Protect == PAGE_READONLY); + Require(VirtualQuery(mapped, &memory, sizeof(memory)) == sizeof(memory) && memory.Protect == PAGE_READWRITE); const auto before = g_accessViolations; const auto handler = AddVectoredExceptionHandler(1, CountAccessViolations); Require(handler != nullptr); @@ -1829,7 +1829,7 @@ static void CheckLargeResidentWritesStopFaulting() { Require(collect() == bytes / 4096 && collect() == 0); } Require(RemoveVectoredExceptionHandler(handler)); - Require(g_accessViolations == before + 1); + Require(g_accessViolations == before); Require(VirtualQuery(mapped, &memory, sizeof(memory)) == sizeof(memory) && memory.Protect == PAGE_READWRITE); Require(mprotect_nid_postfix(mapped, bytes, 1) == 0); Require(collect() == 0 && collect() == 0); @@ -2143,30 +2143,30 @@ static void CheckRemappedDirectMemoryIsChargedOnce() { #ifdef _WIN32 constexpr std::size_t bytes = 0x10000000; const auto committed = [] { - PERFORMANCE_INFORMATION info{}; - info.cb = sizeof(info); - Require(K32GetPerformanceInfo(&info, sizeof(info)) != 0); - return static_cast(info.CommitTotal) * static_cast(info.PageSize); + struct { + PROCESS_MEMORY_COUNTERS_EX counters; + SIZE_T privateWorkingSetSize; + ULONG64 sharedCommitUsage; + } info{}; + info.counters.cb = sizeof(info); + Require(K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&info), sizeof(info)) != 0); + return static_cast(info.counters.PrivateUsage) + static_cast(info.sharedCommitUsage); }; - bool once = false; - for (int attempt = 0; attempt < 3 && !once; ++attempt) { - const auto before = committed(); - std::int64_t phys = 0; - Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &phys) == 0); - void* mapped = nullptr; - Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, phys, 0) == 0); - RequireResidentMapping(mapped); - std::memset(mapped, 0xa5, bytes); - Require(sceKernelMunmap(reinterpret_cast(mapped), bytes) == 0); - void* again = nullptr; - Require(sceKernelMapDirectMemory(&again, bytes, 3, 0, phys, 0) == 0); - const auto* data = static_cast(again); - Require(data[0] == 0xa5 && data[bytes / 2] == 0xa5 && data[bytes - 1] == 0xa5); - once = committed() - before < static_cast(bytes + bytes / 2); - Require(sceKernelMunmap(reinterpret_cast(again), bytes) == 0); - Require(sceKernelReleaseDirectMemory(phys, bytes) == 0); - } - Require(once); + const auto before = committed(); + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, phys, 0) == 0); + RequireResidentMapping(mapped); + std::memset(mapped, 0xa5, bytes); + Require(sceKernelMunmap(reinterpret_cast(mapped), bytes) == 0); + void* again = nullptr; + Require(sceKernelMapDirectMemory(&again, bytes, 3, 0, phys, 0) == 0); + const auto* data = static_cast(again); + Require(data[0] == 0xa5 && data[bytes / 2] == 0xa5 && data[bytes - 1] == 0xa5); + Require(committed() - before < static_cast(bytes + bytes / 2)); + Require(sceKernelMunmap(reinterpret_cast(again), bytes) == 0); + Require(sceKernelReleaseDirectMemory(phys, bytes) == 0); #endif } @@ -3393,7 +3393,7 @@ int main(int argc, char** argv) { CheckPinnedSharedPages(); CheckFailedCollectKeepsWrites(); CheckSingleDirectMappingTracksWritesWithoutFaults(); - CheckLargeResidentWritesStopFaulting(); + CheckLargeResidentWritesDoNotFault(); CheckImportAccessDoesNotInventSharedWrites(); CheckResidentCollectPreservesPartialWrites(); CheckResidentCollectionDuringHostWrites(); diff --git a/core/libs/tests/GuestMemoryPoolStats.cpp b/core/libs/tests/GuestMemoryPoolStats.cpp index 14d926ca31..68372ff07b 100644 --- a/core/libs/tests/GuestMemoryPoolStats.cpp +++ b/core/libs/tests/GuestMemoryPoolStats.cpp @@ -8,6 +8,7 @@ #include #include #include +#include static void Require(bool value) { if (!value) std::abort(); } @@ -45,7 +46,12 @@ int main() { const int http2 = sceHttp2Init(1, ssl, 0x40000, 1); Require(http2 > 0); Http2MemoryPoolStats http2Stats{}; - Require(sceHttp2GetMemoryPoolStats(http2, &http2Stats) == 0 && http2Stats.pool_size == 0x40000); + bool http2StatsThrow = false; + const auto previous = http2Stats; + try { sceHttp2GetMemoryPoolStats(http2, &http2Stats); } + catch (const std::exception&) { http2StatsThrow = true; } + Require(http2StatsThrow && http2Stats.pool_size == previous.pool_size && http2Stats.max_inuse_size == previous.max_inuse_size && + http2Stats.current_inuse_size == previous.current_inuse_size && http2Stats.reserved == previous.reserved); Require(sceHttp2GetMemoryPoolStats(http2, nullptr) == http2NullPointer); Require(sceHttp2GetMemoryPoolStats(12345, nullptr) == http2NullPointer); CheckRejected(sceHttp2GetMemoryPoolStats, 12345, http2InvalidId); diff --git a/core/libs/tests/GuestMove.cpp b/core/libs/tests/GuestMove.cpp index 98d1e59b72..a8a16a5ba2 100644 --- a/core/libs/tests/GuestMove.cpp +++ b/core/libs/tests/GuestMove.cpp @@ -1,5 +1,8 @@ #include "prx/libSceMove/include/Api.hpp" +#include "ProcessParametersFixture.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include +#include #include #include #include @@ -20,6 +23,22 @@ constexpr std::int32_t kUserId = 0x10000000; constexpr std::int32_t kFirstHandle = 0x30B0100; constexpr std::int32_t kHandleStep = 0x100; constexpr std::uint8_t kFill = 0x5A; +constexpr std::uint32_t kLastSdkAllowingNullData = 0x03ffffff; +constexpr std::uint32_t kFirstSdkRequiringData = 0x04000000; + +alignas(16) std::array process{}; + +void InstallProcessParameters() { + const std::uint64_t size = process.size(); + const std::uint32_t magic = 0x4942524f; + std::memcpy(process.data(), &size, sizeof(size)); + std::memcpy(process.data() + 8, &magic, sizeof(magic)); + Require(ProcessParametersFixture::Install(process.data(), process.size())); +} + +void SetCompiledSdkVersion(std::uint32_t version) { + std::memcpy(process.data() + 0x10, &version, sizeof(version)); +} struct Buffer { std::uint8_t bytes[128]; @@ -53,6 +72,9 @@ void RequireNotInit(std::int32_t handle) { Require(sceMoveGetExtensionPortInfo(handle, buffer.bytes) == kNotInit); Require(sceMoveReadStateLatest(handle, buffer.bytes) == kNotInit); Require(sceMoveReadStateRecent(handle, 0, buffer.bytes, &count) == kNotInit); + SetCompiledSdkVersion(kLastSdkAllowingNullData); + Require(sceMoveReadStateRecent(handle, 0, nullptr, &count) == kNotInit); + SetCompiledSdkVersion(kFirstSdkRequiringData); Require(sceMoveSetVibration(handle, 0xFF) == kNotInit); Require(sceMoveSetLightSphere(handle, 0xFF, 0, 0) == kNotInit); Require(sceMoveResetLightSphere(handle) == kNotInit); @@ -84,6 +106,8 @@ void RequireUnknownHandle(std::int32_t handle) { } int main() { + InstallProcessParameters(); + SetCompiledSdkVersion(kFirstSdkRequiringData); RequireNotInit(kFirstHandle); Require(sceMoveInit() == kOk); @@ -111,6 +135,15 @@ int main() { Require(sceMoveReadStateRecent(first, 0, nullptr, &count) == kInvalidArg); Require(sceMoveReadStateRecent(first, 0, buffer.bytes, nullptr) == kInvalidArg); + SetCompiledSdkVersion(kLastSdkAllowingNullData); + Require(sceMoveReadStateRecent(first, 0, nullptr, &count) == kNoControllerConnected); + Require(sceMoveReadStateRecent(first, -1, nullptr, &count) == kInvalidArg); + Require(sceMoveReadStateRecent(first, 0, nullptr, nullptr) == kInvalidArg); + Require(sceMoveReadStateRecent(first + 1, 0, nullptr, &count) == kInvalidHandle); + Require(count == -7); + SetCompiledSdkVersion(kFirstSdkRequiringData); + Require(sceMoveReadStateRecent(first, 0, nullptr, &count) == kInvalidArg); + Require(sceMoveGetDeviceInfo(first, buffer.bytes) == kNoControllerConnected); Require(sceMoveGetExtensionPortInfo(first, buffer.bytes) == kNoControllerConnected); Require(sceMoveReadStateLatest(first, buffer.bytes) == kNoControllerConnected); diff --git a/core/libs/tests/GuestNanosleepLongRequest.cpp b/core/libs/tests/GuestNanosleepLongRequest.cpp index c68722401f..4009390985 100644 --- a/core/libs/tests/GuestNanosleepLongRequest.cpp +++ b/core/libs/tests/GuestNanosleepLongRequest.cpp @@ -17,30 +17,31 @@ static void Require(bool value, std::source_location location = std::source_loca } static constexpr auto ObservationTime = std::chrono::milliseconds(1500); -static std::atomic started{false}; -static std::atomic returned{false}; -static std::int64_t requestedSeconds = 0; -static bool posix = true; -static void* APS5_VABI Sleeper(void*) { - const KernelTimespec request{requestedSeconds, 0}; - started.store(true); - if (posix) nanosleep_nid_postfix(&request, nullptr); +struct SleepRequest { + std::int64_t seconds; + bool posix; + std::atomic started{false}; + std::atomic returned{false}; +}; + +static void* APS5_VABI Sleeper(void* argument) { + auto* sleep = static_cast(argument); + const KernelTimespec request{sleep->seconds, 0}; + sleep->started.store(true); + if (sleep->posix) nanosleep_nid_postfix(&request, nullptr); else sceKernelNanosleep(&request, nullptr); - returned.store(true); + sleep->returned.store(true); return nullptr; } static void SleepsPastTheObservation(std::int64_t seconds, bool usePosix) { - started.store(false); - returned.store(false); - requestedSeconds = seconds; - posix = usePosix; + auto* sleep = new SleepRequest{seconds, usePosix}; Pthread thread = nullptr; - Require(scePthreadCreate(&thread, nullptr, Sleeper, nullptr, nullptr) == 0); - while (!started.load()) std::this_thread::yield(); + Require(scePthreadCreate(&thread, nullptr, Sleeper, sleep, nullptr) == 0); + while (!sleep->started.load()) std::this_thread::yield(); std::this_thread::sleep_for(ObservationTime); - Require(!returned.load()); + Require(!sleep->returned.load()); } int main() { diff --git a/core/libs/tests/GuestNetBooleanReadback.cpp b/core/libs/tests/GuestNetBooleanReadback.cpp new file mode 100644 index 0000000000..c4680dad9f --- /dev/null +++ b/core/libs/tests/GuestNetBooleanReadback.cpp @@ -0,0 +1,66 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include "prx/libSceNet/libSceNet.hpp" +#include "prx/libkernel/Socket/include/SocketPoll.hpp" + + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "check failed at line %u\n", location.line()); + std::abort(); + } +} + +int main() { + Require(sceNetInit_nid_postfix() == 0); + for (const int family : {2, 28}) { + for (const int type : {1, 2}) { + const int socket = sceNetSocket("boolean-readback", family, type, type == 1 ? 6 : 17); + Require(socket >= 0); + std::shared_ptr owner; + std::uintptr_t native = 0; + Require(KernelFindSocketNative_nid_no_patch(socket, &owner, &native)); + for (const int option : {4, 32}) { + if (option == 32 && (family != 2 || type != 2)) continue; + for (const int setting : {1, -1, 0, 4, 0}) { + Require(sceNetSetsockopt(socket, 0xffff, option, &setting, sizeof(setting)) == 0); + for (const std::uint32_t capacity : {0u, 1u, 3u, 4u, 8u}) { + std::array bytes; + bytes.fill(0xa5); + std::uint32_t size = capacity; + const int expected = setting != 0 ? option : 0; + Require(sceNetGetsockopt(socket, 0xffff, option, bytes.data(), &size) == 0); + Require(size == std::min(capacity, 4u)); + Require(std::memcmp(bytes.data(), &expected, size) == 0); + Require(std::all_of(bytes.begin() + size, bytes.end(), [](auto byte) { return byte == 0xa5; })); + } + } + const int enabled = 1; + Require(::setsockopt(native, SOL_SOCKET, option == 4 ? SO_REUSEADDR : SO_BROADCAST, + reinterpret_cast(&enabled), sizeof(enabled)) == 0); + int value = 0; + std::uint32_t size = sizeof(value); + Require(sceNetGetsockopt(socket, 0xffff, option, &value, &size) == 0 && value == option); + Require(sceNetGetsockopt(socket, 0xffff, option, nullptr, &size) == static_cast(0x80410116u)); + Require(*sceNetErrnoLoc() == 22); + Require(sceNetGetsockopt(socket, 0xffff, option, &value, nullptr) == static_cast(0x80410116u)); + } + Require(sceNetSocketClose(socket) == 0); + int value = 0; + std::uint32_t size = sizeof(value); + Require(sceNetGetsockopt(socket, 0xffff, 4, &value, &size) == static_cast(0x80410109u)); + Require(*sceNetErrnoLoc() == 9); + } + } +} diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index 9a0e595f2b..fa070adf54 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -149,6 +149,94 @@ void CheckCallbacks() { Require(sceNetCtlUnregisterCallbackV6(v6) == 0); } +Delivery g_ipcInt; + +void APS5_VABI RecordIpcInt(int event, void* arg) { + ++g_ipcInt.calls; + g_ipcInt.event = event; + g_ipcInt.arg = arg; +} + +int CurrentEvent() { + int state = -1; + Require(sceNetCtlGetState(&state) == 0); + return state == kStateIpObtained ? kEventIpObtained : kEventDisconnected; +} + +void CheckIpcIntGetters() { + Require(sceNetCtlGetStateIpcInt(nullptr) == kErrInvalidAddr); + Require(sceNetCtlGetStateV6IpcInt(nullptr) == kErrInvalidAddr); + int state = -1; + int twin = -2; + Require(sceNetCtlGetStateIpcInt(&state) == 0 && sceNetCtlGetState(&twin) == 0 && state == twin); + state = -1; + twin = -2; + Require(sceNetCtlGetStateV6IpcInt(&state) == 0 && sceNetCtlGetStateV6(&twin) == 0 && state == twin); + + Require(sceNetCtlGetResultIpcInt(kEventDisconnected, nullptr) == kErrInvalidAddr); + Require(sceNetCtlGetResultV6IpcInt(kEventDisconnected, nullptr) == kErrInvalidAddr); + for (const int eventType : {kEventDisconnected, 2, kEventIpObtained}) { + int errorCode = -1; + Require(sceNetCtlGetResultIpcInt(eventType, &errorCode) == 0 && errorCode == 0); + errorCode = -1; + Require(sceNetCtlGetResultV6IpcInt(eventType, &errorCode) == 0 && errorCode == 0); + } + + NetCtlInfo info{}; + Require(sceNetCtlGetInfoIpcInt(14, nullptr) == kErrInvalidAddr); + Require(sceNetCtlGetInfoIpcInt(0, &info) == kErrInvalidCode); + Require(sceNetCtlGetInfoIpcInt(22, &info) == kErrInvalidCode); + Require(sceNetCtlGetInfoIpcInt(0xffffffffu, &info) == kErrInvalidCode); + for (std::uint32_t code = 1; code <= 21; ++code) { + NetCtlInfo internal; + NetCtlInfo expected; + std::memset(&internal, kFill, sizeof(internal)); + std::memset(&expected, kFill, sizeof(expected)); + Require(sceNetCtlGetInfoIpcInt(code, &internal) == sceNetCtlGetInfo(static_cast(code), &expected)); + Require(std::memcmp(&internal, &expected, sizeof(internal)) == 0); + } + + std::uint8_t infoV6[256] = {}; + auto* v6 = reinterpret_cast(infoV6); + Require(sceNetCtlGetInfoV6IpcInt(0, v6) == kErrInvalidCode); + Require(sceNetCtlGetInfoV6IpcInt(6, v6) == kErrInvalidCode); + Require(sceNetCtlGetInfoV6IpcInt(1, nullptr) == kErrInvalidAddr); + int stateV6 = -1; + Require(sceNetCtlGetStateV6(&stateV6) == 0); + if (stateV6 != kStateIpObtained) Require(sceNetCtlGetInfoV6IpcInt(1, v6) == kErrNotConnected); +} + +void CheckIpcIntCallbacks() { + constexpr int invalidId = static_cast(0x80412105); + constexpr int idNotFound = static_cast(0x80412104); + Require(sceNetCtlRegisterCallbackIpcInt(nullptr, nullptr, nullptr) == kErrInvalidAddr); + Require(sceNetCtlRegisterCallbackV6IpcInt(nullptr, nullptr, nullptr) == kErrInvalidAddr); + Require(sceNetCtlUnregisterCallbackIpcInt(-1) == invalidId); + Require(sceNetCtlUnregisterCallbackV6IpcInt(-1) == invalidId); + + const int event = CurrentEvent(); + int probe = 0; + int internal = -1; + Require(sceNetCtlRegisterCallbackIpcInt(RecordIpcInt, &probe, &internal) == 0 && internal == 0); + int shared = -1; + Require(sceNetCtlRegisterCallback(RecordIpcInt, &probe, &shared) == 0 && shared == 1); + Require(sceNetCtlCheckCallback() == 0); + Require(g_ipcInt.calls == 2 && g_ipcInt.event == event && g_ipcInt.arg == &probe); + Require(sceNetCtlUnregisterCallbackIpcInt(shared) == 0); + Require(sceNetCtlUnregisterCallback(internal) == 0); + Require(sceNetCtlUnregisterCallbackIpcInt(internal) == idNotFound); + + int internalV6 = -1; + Require(sceNetCtlRegisterCallbackV6IpcInt(RecordIpcInt, &probe, &internalV6) == 0 && internalV6 == 0); + int sharedV6 = -1; + Require(sceNetCtlRegisterCallbackV6(RecordIpcInt, &probe, &sharedV6) == 0 && sharedV6 == 1); + Require(sceNetCtlCheckCallback() == 0); + Require(g_ipcInt.calls == 2); + Require(sceNetCtlUnregisterCallbackV6IpcInt(sharedV6) == 0); + Require(sceNetCtlUnregisterCallbackV6(internalV6) == 0); + Require(sceNetCtlUnregisterCallbackV6IpcInt(internalV6) == idNotFound); +} + NetCtlInfo Query(int code) { NetCtlInfo info; std::memset(&info, kFill, sizeof(info)); @@ -275,6 +363,8 @@ int main() { Require(sceNetCtlUnregisterCallbackForNpToolkit(-1) == invalidId); Require(sceNetCtlCheckCallbackForNpToolkit() == 0); CheckCallbacks(); + CheckIpcIntGetters(); + CheckIpcIntCallbacks(); Require(sceNetCtlGetStateV6(nullptr) == kErrInvalidAddr); int stateV6 = -1; diff --git a/core/libs/tests/GuestNetDnsInfo.cpp b/core/libs/tests/GuestNetDnsInfo.cpp new file mode 100644 index 0000000000..2dc49bca3e --- /dev/null +++ b/core/libs/tests/GuestNetDnsInfo.cpp @@ -0,0 +1,139 @@ +#include "prx/libSceNet/include/Net.hpp" +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kErrorBase = static_cast(0x80410100); +constexpr int kEfault = 14; +constexpr int kEinval = 22; +constexpr int kEnotinit = 200; + +bool Failed(int result, int error) { return result == (kErrorBase | error) && *sceNetErrnoLoc() == error; } + +void CheckUninitialized() { + NetDnsInfo info{0x08080808u, 0x01010101u}; + const NetDnsInfo before = info; + NetDns6Info info6{}; + std::memset(&info6, 0x5a, sizeof(info6)); + const NetDns6Info before6 = info6; + for (const std::uint32_t flags : {0u, 1u, 0xffffffffu}) { + Require(Failed(sceNetGetDnsInfo(&info, flags), kEnotinit)); + Require(Failed(sceNetSetDnsInfo(&info, flags), kEnotinit)); + Require(Failed(sceNetGetDns6Info(&info6, flags), kEnotinit)); + Require(Failed(sceNetSetDns6Info(&info6, flags), kEnotinit)); + Require(Failed(sceNetGetDnsInfo(nullptr, flags), kEnotinit)); + Require(Failed(sceNetSetDnsInfo(nullptr, flags), kEnotinit)); + Require(Failed(sceNetGetDns6Info(nullptr, flags), kEnotinit)); + Require(Failed(sceNetSetDns6Info(nullptr, flags), kEnotinit)); + Require(std::memcmp(&info, &before, sizeof(info)) == 0); + Require(std::memcmp(&info6, &before6, sizeof(info6)) == 0); + } +} + +void CheckDns() { + NetDnsInfo info{0x08080808u, 0}; + Require(Failed(sceNetSetDnsInfo(&info, 1), kEinval)); + NetDnsInfo empty{0, 0x01010101u}; + Require(Failed(sceNetSetDnsInfo(&empty, 0), kEinval)); + + Require(sceNetSetDnsInfo(&info, 0) == 0); + NetDnsInfo read{1, 1}; + Require(sceNetGetDnsInfo(&read, 0) == 1 && read.primaryAddress == 0x08080808u && read.secondaryAddress == 0); + info.secondaryAddress = 0x04040808u; + Require(sceNetSetDnsInfo(&info, 0) == 0); + Require(sceNetGetDnsInfo(&read, 0) == 2 && read.primaryAddress == 0x08080808u && read.secondaryAddress == 0x04040808u); + Require(sceNetSetDnsInfo(nullptr, 0) == 0); + Require(sceNetGetDnsInfo(&read, 0) >= 0); + + NetDns6Info info6{}; + Require(Failed(sceNetGetDnsInfo(nullptr, 0), kEinval)); + Require(Failed(sceNetGetDns6Info(nullptr, 0), kEinval)); + for (const std::uint32_t flags : {1u, 2u, 0xffffffffu}) { + NetDnsInfo unchanged{0x5a5a5a5au, 0x5a5a5a5au}; + const NetDnsInfo before = unchanged; + std::memset(&info6, 0x5a, sizeof(info6)); + const NetDns6Info before6 = info6; + Require(Failed(sceNetGetDnsInfo(&unchanged, flags), kEinval)); + Require(Failed(sceNetGetDns6Info(&info6, flags), kEinval)); + Require(Failed(sceNetGetDnsInfo(nullptr, flags), kEinval)); + Require(Failed(sceNetGetDns6Info(nullptr, flags), kEinval)); + Require(Failed(sceNetSetDnsInfo(nullptr, flags), kEinval)); + Require(Failed(sceNetSetDns6Info(nullptr, flags), kEinval)); + Require(std::memcmp(&unchanged, &before, sizeof(before)) == 0); + Require(std::memcmp(&info6, &before6, sizeof(before6)) == 0); + } + std::memset(&info6, 0xff, sizeof(info6)); + Require(sceNetGetDns6Info(&info6, 0) == 0); + for (const auto byte : info6.primaryAddress.bytes) Require(byte == 0); + for (const auto byte : info6.secondaryAddress.bytes) Require(byte == 0); + + NetDns6Info set6{}; + Require(Failed(sceNetSetDns6Info(&set6, 0), kEinval)); + set6.primaryAddress.bytes[0] = 0x20; + set6.primaryAddress.bytes[15] = 0x01; + Require(Failed(sceNetSetDns6Info(&set6, 2), kEinval)); + Require(sceNetSetDns6Info(&set6, 0) == 0); + Require(sceNetGetDns6Info(&info6, 0) == 1 && std::memcmp(&info6, &set6, sizeof(set6)) == 0); + set6.secondaryAddress.bytes[0] = 0x26; + Require(sceNetSetDns6Info(&set6, 0) == 0); + Require(sceNetGetDns6Info(&info6, 0) == 2 && std::memcmp(&info6, &set6, sizeof(set6)) == 0); + Require(sceNetSetDns6Info(nullptr, 0) == 0); + Require(sceNetGetDns6Info(&info6, 0) == 0); +} + +} + +void* UnreadablePage() { +#ifdef _WIN32 + return VirtualAlloc(nullptr, 4096, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS); +#else + void* page = mmap(nullptr, 4096, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + return page == MAP_FAILED ? nullptr : page; +#endif +} + +void CheckUnreadableInfo() { + void* page = UnreadablePage(); + Require(page != nullptr); + NetDnsInfo info{0x08080808u, 0}; + Require(sceNetSetDnsInfo(&info, 0) == 0); + Require(Failed(sceNetSetDnsInfo(static_cast(page), 1), kEinval)); + Require(Failed(sceNetSetDnsInfo(static_cast(page), 0), kEfault)); + NetDnsInfo read{}; + Require(sceNetGetDnsInfo(&read, 0) == 1 && read.primaryAddress == 0x08080808u); + Require(sceNetSetDnsInfo(nullptr, 0) == 0); + + NetDns6Info info6{}; + info6.primaryAddress.bytes[0] = 0x20; + Require(sceNetSetDns6Info(&info6, 0) == 0); + Require(Failed(sceNetSetDns6Info(static_cast(page), 1), kEinval)); + Require(Failed(sceNetSetDns6Info(static_cast(page), 0), kEfault)); + NetDns6Info read6{}; + Require(sceNetGetDns6Info(&read6, 0) == 1 && std::memcmp(&read6, &info6, sizeof(info6)) == 0); + Require(sceNetSetDns6Info(nullptr, 0) == 0); +#ifdef _WIN32 + Require(VirtualFree(page, 0, MEM_RELEASE) != 0); +#else + Require(munmap(page, 4096) == 0); +#endif +} + +int main() { + CheckUninitialized(); + Require(sceNetInit_nid_postfix() == 0); + CheckDns(); + CheckUnreadableInfo(); + Require(sceNetTerm() == 0); + CheckUninitialized(); + return 0; +} diff --git a/core/libs/tests/GuestNetKeepalive.cpp b/core/libs/tests/GuestNetKeepalive.cpp new file mode 100644 index 0000000000..d8d8624a88 --- /dev/null +++ b/core/libs/tests/GuestNetKeepalive.cpp @@ -0,0 +1,115 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include "prx/libSceNet/libSceNet.hpp" +#include "prx/libkernel/libkernel.hpp" +#include "prx/libkernel/Socket/include/SocketPoll.hpp" + + +static void Require(bool condition) { + if (!condition) std::abort(); +} + +static bool Failed(std::int64_t result, int error) { + return result == static_cast(0x80410100u | static_cast(error)) && *sceNetErrnoLoc() == error; +} + +static void CheckKeepalive() { + for (const int family : {2, 28}) { + for (const int type : {1, 2}) { + const int socket = sceNetSocket("keepalive", family, type, type == 1 ? 6 : 17); + Require(socket >= 0); + int value = -1; + std::uint32_t size = sizeof(value); + Require(sceNetGetsockopt(socket, 0xffff, 8, &value, &size) == 0 && value == 0 && size == sizeof(value)); + for (const int setting : {1, -1, 0, 8, 0}) { + Require(sceNetSetsockopt(socket, 0xffff, 8, &setting, sizeof(setting)) == 0); + size = sizeof(value); + Require(sceNetGetsockopt(socket, 0xffff, 8, &value, &size) == 0 && value == (setting ? 8 : 0)); + if (type == 1) { + std::shared_ptr owner; + std::uintptr_t native = 0; + Require(KernelFindSocketNative_nid_no_patch(socket, &owner, &native)); + int enabled = 0; +#ifdef _WIN32 + int length = sizeof(enabled); +#else + socklen_t length = sizeof(enabled); +#endif + Require(::getsockopt(native, SOL_SOCKET, SO_KEEPALIVE, reinterpret_cast(&enabled), &length) == 0); + Require((enabled != 0) == (setting != 0)); + } + } + const int enabled = 1; + Require(Failed(sceNetSetsockopt(socket, 0xffff, 8, &enabled, sizeof(enabled) - 1), 22)); + Require(Failed(sceNetSetsockopt(socket, 0xffff, 8, nullptr, sizeof(enabled)), 22)); + Require(setsockopt_nid_postfix(socket, 0xffff, 8, &enabled, sizeof(enabled)) == 0); + const int other = sceNetSocket("keepalive-other", family, type, type == 1 ? 6 : 17); + Require(other >= 0); + size = sizeof(value); + Require(sceNetGetsockopt(other, 0xffff, 8, &value, &size) == 0 && value == 0); + Require(sceNetSocketClose(other) == 0); + Require(Failed(sceNetGetsockopt(socket, 0xffff, 8, nullptr, &size), 22)); + Require(Failed(sceNetGetsockopt(socket, 0xffff, 8, &value, nullptr), 22)); + for (const std::uint32_t length : {0u, 1u, 3u, 4u, 8u}) { + std::array bytes; + bytes.fill(0xa5); + size = length; + const int expected = 8; + Require(sceNetGetsockopt(socket, 0xffff, 8, bytes.data(), &size) == 0 && size == std::min(length, 4u)); + Require(std::memcmp(bytes.data(), &expected, size) == 0); + Require(std::all_of(bytes.begin() + size, bytes.end(), [](auto byte) { return byte == 0xa5; })); + } + Require(sceNetSocketClose(socket) == 0); + size = sizeof(value); + Require(Failed(sceNetGetsockopt(socket, 0xffff, 8, &value, &size), 9)); + Require(Failed(sceNetSetsockopt(socket, 0xffff, 8, &enabled, sizeof(enabled)), 9)); + } + } +} + +int main() { + Require(sceNetInit_nid_postfix() == 0); + CheckKeepalive(); + const auto readFlag = [](int socket) { + int value = -1; + std::uint32_t size = sizeof(value); + Require(sceNetGetsockopt(socket, 0xffff, 8, &value, &size) == 0 && size == sizeof(value)); + return value; + }; + for (const int family : {2, 28}) { + const int listener = sceNetSocket("keepalive-listener", family, 1, 6); + const int client = sceNetSocket("keepalive-client", family, 1, 6); + Require(listener >= 0 && client >= 0); + const int on = 1; + const int off = 0; + Require(sceNetSetsockopt(listener, 0xffff, 8, &on, sizeof(on)) == 0); + std::array address{}; + std::uint32_t size = family == 2 ? 16 : 28; + address[0] = size; + address[1] = family; + if (family == 2) { + address[4] = 127; + address[7] = 1; + } else { + address[23] = 1; + } + Require(sceNetBind_nid_postfix(listener, address.data(), size) == 0); + Require(sceNetListen(listener, 1) == 0); + Require(sceNetGetsockname(listener, address.data(), &size) == 0); + Require(sceNetConnect(client, address.data(), size) == 0); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0 && readFlag(accepted) == 8 && readFlag(client) == 0); + Require(sceNetSetsockopt(listener, 0xffff, 8, &off, sizeof(off)) == 0); + Require(readFlag(listener) == 0 && readFlag(accepted) == 8); + for (const int socket : {accepted, client, listener}) Require(sceNetSocketClose(socket) == 0); + } +} diff --git a/core/libs/tests/GuestNetNodelayReadback.cpp b/core/libs/tests/GuestNetNodelayReadback.cpp new file mode 100644 index 0000000000..ad296cdba6 --- /dev/null +++ b/core/libs/tests/GuestNetNodelayReadback.cpp @@ -0,0 +1,77 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#include +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include "prx/libSceNet/libSceNet.hpp" +#include "prx/libkernel/Socket/include/SocketPoll.hpp" + + +static void Require(bool condition, std::source_location where = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "check failed at line %u\n", where.line()); + std::abort(); + } +} + +static void CheckValue(int socket, int expected) { + for (const std::uint32_t capacity : {0u, 1u, 2u, 3u, 4u, 8u}) { + std::array bytes; + bytes.fill(0xa5); + auto size = capacity; + Require(sceNetGetsockopt(socket, 6, 1, bytes.data(), &size) == 0); + Require(size == std::min(capacity, 4u)); + Require(std::memcmp(bytes.data(), &expected, size) == 0); + Require(std::all_of(bytes.begin() + size, bytes.end(), [](auto value) { return value == 0xa5; })); + } +} + +int main() { + Require(sceNetInit_nid_postfix() == 0); + for (const int family : {2, 28}) { + const int socket = sceNetSocket("nodelay", family, 1, 6); + const int other = sceNetSocket("nodelay-other", family, 1, 0); + const int datagram = sceNetSocket("nodelay-datagram", family, 2, 17); + Require(socket >= 0 && other >= 0 && datagram >= 0); + CheckValue(socket, 0); + for (const int requested : {1, 0, -1, 256, 0}) { + Require(sceNetSetsockopt(socket, 6, 1, &requested, sizeof(requested)) == 0); + CheckValue(socket, requested != 0 ? 4 : 0); + CheckValue(other, 0); + } + std::shared_ptr owner; + std::uintptr_t native = 0; + Require(KernelFindSocketNative_nid_no_patch(socket, &owner, &native)); + const int enabled = 1; + Require(::setsockopt(native, IPPROTO_TCP, TCP_NODELAY, + reinterpret_cast(&enabled), sizeof(enabled)) == 0); + CheckValue(socket, 4); + std::uint32_t size = 0; + int value = 99; + Require(sceNetGetsockopt(socket, 6, 1, nullptr, &size) < 0 && *sceNetErrnoLoc() == 22); + Require(sceNetGetsockopt(socket, 6, 1, &value, nullptr) < 0 && *sceNetErrnoLoc() == 22); + for (const std::uint32_t capacity : {0u, 3u, 4u}) { + size = capacity; + Require(sceNetGetsockopt(datagram, 6, 1, &value, &size) < 0 && *sceNetErrnoLoc() == 22); + Require(size == capacity && value == 99); + Require(sceNetGetsockopt(socket, 6, 0x7fffffff, &value, &size) < 0); + Require(*sceNetErrnoLoc() == (capacity < 4 ? 22 : 42)); + Require(size == capacity && value == 99); + } + owner.reset(); + for (const int descriptor : {socket, other, datagram}) Require(sceNetSocketClose(descriptor) == 0); + size = 0; + Require(sceNetGetsockopt(socket, 6, 1, &value, &size) < 0 && *sceNetErrnoLoc() == 9); + Require(size == 0 && value == 99); + } +} diff --git a/core/libs/tests/GuestNetOobInline.cpp b/core/libs/tests/GuestNetOobInline.cpp new file mode 100644 index 0000000000..6a8e4c4cf5 --- /dev/null +++ b/core/libs/tests/GuestNetOobInline.cpp @@ -0,0 +1,135 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include "prx/libSceNet/libSceNet.hpp" +#include "prx/libkernel/Socket/include/SocketPoll.hpp" + + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "check failed at line %u\n", location.line()); + std::abort(); + } +} + +static void CheckFlag(int socket, int expected) { + for (const std::uint32_t capacity : {0u, 1u, 3u, 4u, 8u}) { + std::array bytes; + bytes.fill(0xa5); + std::uint32_t size = capacity; + Require(sceNetGetsockopt(socket, 0xffff, 0x100, bytes.data(), &size) == 0 && size == std::min(capacity, 4u)); + Require(std::memcmp(bytes.data(), &expected, size) == 0); + Require(std::all_of(bytes.begin() + size, bytes.end(), [](auto byte) { return byte == 0xa5; })); + } +} + +static void CheckInheritance(int family, int beforeListen, bool changeAfterListen) { + std::fprintf(stderr, "inheritance family=%d beforeListen=%d changeAfterListen=%d\n", + family, beforeListen, changeAfterListen); + const int listener = sceNetSocket("inheritance-listener", family, 1, 6); + const int client = sceNetSocket("inheritance-client", family, 1, 6); + Require(listener >= 0 && client >= 0); + Require(sceNetSetsockopt(listener, 0xffff, 0x100, &beforeListen, sizeof(beforeListen)) == 0); + CheckFlag(listener, beforeListen != 0 ? 256 : 0); + std::array address{}; + std::uint32_t size = family == 2 ? 16 : 28; + address[0] = size; + address[1] = family; + if (family == 2) { address[4] = 127; address[7] = 1; } + else address[23] = 1; + Require(sceNetBind_nid_postfix(listener, address.data(), size) == 0); + Require(sceNetListen(listener, 1) == 0); + const int setting = changeAfterListen ? 1 - beforeListen : beforeListen; + if (changeAfterListen) + Require(sceNetSetsockopt(listener, 0xffff, 0x100, &setting, sizeof(setting)) == 0); + CheckFlag(listener, setting != 0 ? 256 : 0); + Require(sceNetGetsockname(listener, address.data(), &size) == 0); + Require(sceNetConnect(client, address.data(), size) == 0); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0); + CheckFlag(accepted, setting != 0 ? 256 : 0); + CheckFlag(client, 0); + const int opposite = 1 - setting; + Require(sceNetSetsockopt(listener, 0xffff, 0x100, &opposite, sizeof(opposite)) == 0); + CheckFlag(listener, opposite != 0 ? 256 : 0); + CheckFlag(accepted, setting != 0 ? 256 : 0); + Require(sceNetSetsockopt(listener, 0xffff, 0x100, &setting, sizeof(setting)) == 0); + Require(sceNetSetsockopt(accepted, 0xffff, 0x100, &opposite, sizeof(opposite)) == 0); + CheckFlag(accepted, opposite != 0 ? 256 : 0); + CheckFlag(listener, setting != 0 ? 256 : 0); + CheckFlag(client, 0); + for (const int socket : {accepted, client, listener}) Require(sceNetSocketClose(socket) == 0); +} + +int main() { + Require(sceNetInit_nid_postfix() == 0); + for (const int family : {2, 28}) { + for (const int beforeListen : {0, 1}) { + for (const bool changeAfterListen : {false, true}) + CheckInheritance(family, beforeListen, changeAfterListen); + } + const int datagram = sceNetSocket("inline-datagram", family, 2, 17); + const int listener = sceNetSocket("inline-listener", family, 1, 6); + const int client = sceNetSocket("inline-client", family, 1, 6); + Require(datagram >= 0 && listener >= 0 && client >= 0); + for (const int socket : {datagram, listener}) { + CheckFlag(socket, 0); + for (const int setting : {1, -1, 0, 256, 0, 1}) { + Require(sceNetSetsockopt(socket, 0xffff, 0x100, &setting, sizeof(setting)) == 0); + CheckFlag(socket, setting != 0 ? 256 : 0); + } + const int on = 1; + Require(sceNetSetsockopt(socket, 0xffff, 0x100, &on, 3) == static_cast(0x80410116u)); + Require(*sceNetErrnoLoc() == 22); + Require(sceNetSetsockopt(socket, 0xffff, 0x100, nullptr, 4) == static_cast(0x80410116u)); + } + CheckFlag(client, 0); + std::array address{}; + std::uint32_t size = family == 2 ? 16 : 28; + address[0] = size; + address[1] = family; + if (family == 2) { address[4] = 127; address[7] = 1; } + else address[23] = 1; + Require(sceNetBind_nid_postfix(listener, address.data(), size) == 0); + Require(sceNetListen(listener, 1) == 0); + Require(sceNetGetsockname(listener, address.data(), &size) == 0); + Require(sceNetConnect(client, address.data(), size) == 0); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0); + CheckFlag(accepted, 256); + const int timeout = 500000; + Require(sceNetSetsockopt(accepted, 0xffff, 0x1106, &timeout, sizeof(timeout)) == 0); + std::shared_ptr owner; + std::uintptr_t native = 0; + Require(KernelFindSocketNative_nid_no_patch(client, &owner, &native)); + Require(::send(native, "A", 1, 0) == 1); + Require(::send(native, "!", 1, MSG_OOB) == 1); + Require(::send(native, "B", 1, 0) == 1); + std::array received{}; + std::size_t count = 0; + while (count < received.size()) { + const auto result = sceNetRecv(accepted, received.data() + count, received.size() - count, 0); + Require(result > 0); + count += result; + } + Require(std::memcmp(received.data(), "A!B", 3) == 0); + const int off = 0; + Require(sceNetSetsockopt(listener, 0xffff, 0x100, &off, sizeof(off)) == 0); + CheckFlag(listener, 0); + CheckFlag(accepted, 256); + for (const int socket : {accepted, client, listener, datagram}) Require(sceNetSocketClose(socket) == 0); + int value = 0; + size = sizeof(value); + Require(sceNetGetsockopt(listener, 0xffff, 0x100, &value, &size) == static_cast(0x80410109u)); + } +} diff --git a/core/libs/tests/GuestNetProtocol.cpp b/core/libs/tests/GuestNetProtocol.cpp new file mode 100644 index 0000000000..038c59f77d --- /dev/null +++ b/core/libs/tests/GuestNetProtocol.cpp @@ -0,0 +1,71 @@ +#include +#include +#include +#include +#include +#include +#include +#include "prx/libSceNet/libSceNet.hpp" + + +static void Require(bool condition, std::source_location where = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "check failed at line %u\n", where.line()); + std::abort(); + } +} + +static void CheckProtocol(int socket, int expected) { + for (const std::uint32_t capacity : {0u, 1u, 3u, 4u, 8u}) { + std::array bytes; + bytes.fill(0xa5); + auto size = capacity; + Require(sceNetGetsockopt(socket, 0xffff, 0x1016, bytes.data(), &size) == 0); + Require(size == std::min(capacity, 4u)); + Require(std::memcmp(bytes.data(), &expected, size) == 0); + Require(std::all_of(bytes.begin() + size, bytes.end(), [](auto value) { return value == 0xa5; })); + } +} + +int main() { + Require(sceNetInit_nid_postfix() == 0); + for (const int family : {2, 28}) { + for (const int type : {1, 2}) { + const int expected = type == 1 ? 6 : 17; + for (const int protocol : {0, expected}) { + const int socket = sceNetSocket("protocol", family, type, protocol); + Require(socket >= 0); + CheckProtocol(socket, expected); + std::uint32_t size = 4; + int value = 99; + Require(sceNetGetsockopt(socket, 0xffff, 0x1016, nullptr, &size) < 0 && *sceNetErrnoLoc() == 22); + Require(sceNetGetsockopt(socket, 0xffff, 0x1016, &value, nullptr) < 0 && *sceNetErrnoLoc() == 22); + Require(value == 99 && size == 4); + Require(sceNetSetsockopt(socket, 0xffff, 0x1016, &value, sizeof(value)) < 0); + CheckProtocol(socket, expected); + Require(sceNetSocketClose(socket) == 0); + Require(sceNetGetsockopt(socket, 0xffff, 0x1016, &value, &size) < 0 && *sceNetErrnoLoc() == 9); + Require(value == 99 && size == 4); + } + } + const int listener = sceNetSocket("protocol-listener", family, 1, 0); + const int client = sceNetSocket("protocol-client", family, 1, 6); + Require(listener >= 0 && client >= 0); + std::array address{}; + std::uint32_t size = family == 2 ? 16 : 28; + address[0] = size; + address[1] = family; + if (family == 2) { address[4] = 127; address[7] = 1; } + else address[23] = 1; + Require(sceNetBind_nid_postfix(listener, address.data(), size) == 0); + Require(sceNetListen(listener, 1) == 0); + CheckProtocol(listener, 6); + Require(sceNetGetsockname(listener, address.data(), &size) == 0); + Require(sceNetConnect(client, address.data(), size) == 0); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0); + CheckProtocol(accepted, 6); + CheckProtocol(client, 6); + for (const int socket : {accepted, client, listener}) Require(sceNetSocketClose(socket) == 0); + } +} diff --git a/core/libs/tests/GuestNetSocketState.cpp b/core/libs/tests/GuestNetSocketState.cpp new file mode 100644 index 0000000000..2101d0c167 --- /dev/null +++ b/core/libs/tests/GuestNetSocketState.cpp @@ -0,0 +1,74 @@ +#include +#include +#include +#include +#include +#include "prx/libSceNet/libSceNet.hpp" + + +static void Require(bool condition) { + if (!condition) std::abort(); +} + +static void CheckOption(int socket, int option, int expected) { + for (const std::uint32_t capacity : {0u, 1u, 3u, 4u, 8u}) { + std::array bytes; + bytes.fill(0xa5); + std::uint32_t size = capacity; + Require(sceNetGetsockopt(socket, 0xffff, option, bytes.data(), &size) == 0); + Require(size == std::min(capacity, 4u)); + Require(std::memcmp(bytes.data(), &expected, size) == 0); + Require(std::all_of(bytes.begin() + size, bytes.end(), [](auto byte) { return byte == 0xa5; })); + } +} + +int main() { + Require(sceNetInit_nid_postfix() == 0); + for (const int family : {2, 28}) { + const int datagram = sceNetSocket("state-datagram", family, 2, 17); + const int listener = sceNetSocket("state-listener", family, 1, 6); + const int client = sceNetSocket("state-client", family, 1, 6); + Require(datagram >= 0 && listener >= 0 && client >= 0); + CheckOption(datagram, 0x1008, 2); + CheckOption(datagram, 2, 0); + CheckOption(listener, 0x1008, 1); + CheckOption(listener, 2, 0); + std::array address{}; + const std::uint32_t addressSize = family == 2 ? 16 : 28; + address[0] = addressSize; + address[1] = family; + if (family == 2) { + address[4] = 127; + address[7] = 1; + } else { + address[23] = 1; + } + Require(sceNetBind_nid_postfix(listener, address.data(), addressSize) == 0); + Require(sceNetListen(listener, 1) == 0); + CheckOption(listener, 2, 2); + CheckOption(client, 2, 0); + std::uint32_t size = addressSize; + Require(sceNetGetsockname(listener, address.data(), &size) == 0); + Require(sceNetConnect(client, address.data(), size) == 0); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0); + CheckOption(listener, 2, 2); + CheckOption(client, 0x1008, 1); + CheckOption(client, 2, 0); + CheckOption(accepted, 0x1008, 1); + CheckOption(accepted, 2, 0); + int value = 0; + size = sizeof(value); + for (const int option : {2, 0x1008}) { + Require(sceNetGetsockopt(listener, 0xffff, option, nullptr, &size) == static_cast(0x80410116u)); + Require(*sceNetErrnoLoc() == 22); + Require(sceNetGetsockopt(listener, 0xffff, option, &value, nullptr) == static_cast(0x80410116u)); + Require(*sceNetErrnoLoc() == 22); + } + for (const int socket : {accepted, client, listener, datagram}) Require(sceNetSocketClose(socket) == 0); + for (const int option : {2, 0x1008}) { + Require(sceNetGetsockopt(listener, 0xffff, option, &value, &size) == static_cast(0x80410109u)); + Require(*sceNetErrnoLoc() == 9); + } + } +} diff --git a/core/libs/tests/GuestNpCommonRuntime.cpp b/core/libs/tests/GuestNpCommonRuntime.cpp index 7b2ad35b4c..1747776629 100644 --- a/core/libs/tests/GuestNpCommonRuntime.cpp +++ b/core/libs/tests/GuestNpCommonRuntime.cpp @@ -30,7 +30,10 @@ constexpr int NpOutOfRange = static_cast(0x8055800E); constexpr int NpInvalidArgument = static_cast(0x80558009); constexpr int NpEventFlagBusy = static_cast(0x8055800C); constexpr int NpSemaBusy = static_cast(0x8055800D); +constexpr int KernelNotPermitted = static_cast(0x80020001); +constexpr int KernelNoSuchObject = static_cast(0x80020003); constexpr std::uint32_t EventFlagWaitAnd = 1; +constexpr std::uint32_t EventFlagShared = 0x100; void Require(bool value, const char* message) { if (value) return; @@ -67,6 +70,20 @@ void TestSemaphoresAndFlags() { Require(sceNpDeleteEventFlag(flag) == 0, "the event flag is deleted"); } +void TestSharedEventFlags() { + KernelEventFlag creator {}; + Require(sceNpCreateEventFlag(&creator, "NpSharedFlag", EventFlagShared, 0) == 0, "a shared event flag is created"); + KernelEventFlag opened {}; + Require(sceNpOpenEventFlag(&opened, "NpSharedFlag") == 0 && opened != creator, "the shared event flag is opened by name"); + std::uint64_t result = 0; + Require(sceNpSetEventFlag(opened, 2) == 0 && sceNpPollEventFlag(creator, 2, EventFlagWaitAnd, &result) == 0 && result == 2, + "a bit set through the opened handle is polled through the creator"); + Require(sceNpCloseEventFlag(creator) == KernelNotPermitted, "the creator handle is not closed"); + Require(sceNpCloseEventFlag(opened) == 0, "the opened handle is closed"); + Require(sceNpCloseEventFlag(opened) == KernelNoSuchObject, "a closed handle is gone"); + Require(sceNpDeleteEventFlag(creator) == 0, "the creator deletes the shared event flag"); +} + void TestThreads() { Require(sceNpThreadGetId(nullptr) == NpInvalidArgument, "a null thread id output is refused"); Pthread thread {}; @@ -400,6 +417,7 @@ void TestHexAndCopies() { int main() { TestAtomics(); TestSemaphoresAndFlags(); + TestSharedEventFlags(); TestThreads(); TestHeaps(); TestBase64(); diff --git a/core/libs/tests/GuestNpCppWebApiOffline.cpp b/core/libs/tests/GuestNpCppWebApiOffline.cpp index ed22df2b30..5317a857ed 100644 --- a/core/libs/tests/GuestNpCppWebApiOffline.cpp +++ b/core/libs/tests/GuestNpCppWebApiOffline.cpp @@ -12,6 +12,84 @@ static void Require(bool value, const char* what) { std::abort(); } +static constexpr int ParameterNotInitialized = static_cast(0x80553502); +static constexpr int ParameterInvalidArgument = static_cast(0x80553503); +static constexpr int ParameterAlreadyInitialized = static_cast(0x8055350f); +static constexpr int ParameterTerminatedUninitialized = static_cast(0x80553510); + +struct ParameterLifecycle { + int (APS5_VABI* construct)(void*); + int (APS5_VABI* destroy)(void*); + int (APS5_VABI* terminate)(void*); + int (APS5_VABI* setText)(void*, const char*); +}; + +template +static void TestParameterLifecycle(const ParameterLifecycle& lifecycle, TInitialize initialize) { + std::uint64_t context = 0; + alignas(16) unsigned char parameter[0x20]; + std::memset(parameter, 0x5a, sizeof(parameter)); + Require(lifecycle.construct(parameter) == 0, "a parameter constructor failed"); + for (std::size_t index = 0; index < sizeof(parameter); ++index) + Require(parameter[index] == (index < 0x10 ? 0 : 0x5a), "a parameter constructor wrote the wrong bytes"); + Require(lifecycle.setText(parameter, "text") == ParameterNotInitialized, "a setter accepted an uninitialized parameter"); + Require(lifecycle.terminate(parameter) == ParameterTerminatedUninitialized, "terminate accepted an uninitialized parameter"); + Require(initialize(parameter, nullptr, true) == ParameterInvalidArgument, "initialize accepted a null context"); + Require(initialize(parameter, &context, true) == ParameterNotInitialized, "initialize accepted an uninitialized context"); + Require(_ZN3sce2Np9CppWebApi6Common10initializeERKNS2_10InitParamsERNS2_10LibContextE(nullptr, &context) == 0, "the context did not initialize"); + Require(initialize(parameter, &context, false) == ParameterInvalidArgument, "initialize accepted a null argument"); + Require(lifecycle.setText(parameter, "text") == ParameterNotInitialized, "a failed initialize left the parameter initialized"); + Require(initialize(parameter, &context, true) == 0, "initialize failed"); + Require(initialize(parameter, &context, true) == ParameterAlreadyInitialized, "initialize ran twice"); + Require(lifecycle.setText(parameter, nullptr) == ParameterInvalidArgument, "a setter accepted a null string"); + Require(lifecycle.setText(parameter, "text") == 0, "a setter failed"); + Require(lifecycle.terminate(parameter) == 0, "terminate failed"); + Require(lifecycle.terminate(parameter) == ParameterTerminatedUninitialized, "terminate ran twice"); + Require(initialize(parameter, &context, true) == 0, "initialize after terminate failed"); + Require(lifecycle.terminate(parameter) == 0, "the second terminate failed"); + Require(lifecycle.destroy(parameter) == 0, "a parameter destructor failed"); + for (std::size_t index = 0; index < sizeof(parameter); ++index) + Require(parameter[index] == (index < 0x10 ? 0 : 0x5a), "a parameter lifecycle wrote the wrong bytes"); + Require(_ZN3sce2Np9CppWebApi6Common9terminateERNS2_10LibContextE(&context) == 0, "the context did not terminate"); +} + +static void TestParameterLifecycles() { + TestParameterLifecycle( + { + _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainerC1Ev, + _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainerD1Ev, + _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer9terminateEv, + _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer15setcontainerIdsEPKc, + }, + [](void* parameter, const std::uint64_t* context, bool present) { + const bool before = _ZNK3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer13isInitializedEv(parameter); + const int result = present ? _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer10initializeEPNS1_6Common10LibContextE(parameter, context) : ParameterInvalidArgument; + Require(_ZNK3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer13isInitializedEv(parameter) == (before || result == 0), "isInitialized disagrees with initialize"); + Require(_ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer15setserviceLabelEi(parameter, 1) == 0, "setserviceLabel failed"); + return result; + }); + TestParameterLifecycle( + { + _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUserC1Ev, + _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUserD1Ev, + _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser9terminateEv, + _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser9setfieldsEPKc, + }, + [](void* parameter, const std::uint64_t* context, bool present) { + return _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser10initializeEPNS1_6Common10LibContextEPKcSA_(parameter, context, "me", present ? "slot" : nullptr); + }); + TestParameterLifecycle( + { + _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadDataC1Ev, + _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadDataD1Ev, + _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData9terminateEv, + _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData11setobjectIdEPKc, + }, + [](void* parameter, const std::uint64_t* context, bool present) { + return _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData10initializeEPNS1_6Common10LibContextEPKci(parameter, context, present ? "me" : nullptr, 0); + }); +} + static void TestIteratorDestructors() { unsigned char iterator[0x20]; std::memset(iterator, 0x5a, sizeof(iterator)); @@ -24,6 +102,7 @@ static void TestIteratorDestructors() { } int main() { + TestParameterLifecycles(); TestIteratorDestructors(); std::puts("NpCppWebApi offline tests passed"); return 0; diff --git a/core/libs/tests/GuestNpGameIntent.cpp b/core/libs/tests/GuestNpGameIntent.cpp index dd362cdad4..8e74607008 100644 --- a/core/libs/tests/GuestNpGameIntent.cpp +++ b/core/libs/tests/GuestNpGameIntent.cpp @@ -4,6 +4,7 @@ #include #include +#include #include static void Require(bool value, const char* what) { @@ -23,6 +24,8 @@ static bool Throws(TCall call) { } int main() { + constexpr int kNotInitialized = -2141898750; + constexpr int kInvalidArgument = -2141898748; constexpr int kIntentNotFound = -2141898746; constexpr int kValueNotFound = -2141898745; @@ -41,22 +44,75 @@ int main() { Require(info.size == sizeof(NpGameIntentInfo), "info size mismatch"); Require(info.user_id == -1, "info user_id mismatch"); + char preBuf[64]{}; + Require(sceNpGameIntentGetPropertyValueString(nullptr, "key", preBuf, sizeof(preBuf)) == kNotInitialized, + "uninitialized get property string with null data must report not-initialized"); + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "key", preBuf, sizeof(preBuf)) == kNotInitialized, + "uninitialized get property string with valid data must report not-initialized"); + Require(sceNpGameIntentReceiveIntent(nullptr) == kNotInitialized, + "uninitialized receive intent with null pointer must report not-initialized"); + Require(sceNpGameIntentReceiveIntent(&info) == kNotInitialized, + "uninitialized receive intent with valid pointer must report not-initialized"); + Require(sceNpGameIntentInitialize(&initParam) == 0, "initialization failed"); - Require(sceNpGameIntentReceiveIntent(&info) == kIntentNotFound, "receive intent mismatch"); + Require(sceNpGameIntentReceiveIntent(nullptr) == kInvalidArgument, "null receive intent did not fail"); + Require(sceNpGameIntentReceiveIntent(&info) == kInvalidArgument, "mismatched struct size must report invalid argument"); + info.size = 0; + Require(sceNpGameIntentReceiveIntent(&info) == kInvalidArgument, "zero size must report invalid argument"); + info.size = 0x413f; + Require(sceNpGameIntentReceiveIntent(&info) == kInvalidArgument, "0x413f size must report invalid argument"); + info.size = 0x4141; + Require(sceNpGameIntentReceiveIntent(&info) == kInvalidArgument, "0x4141 size must report invalid argument"); + info.size = sizeof(NpGameIntentInfo); char buf[64]{}; - Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "key", buf, sizeof(buf)) == kValueNotFound, + Require(sceNpGameIntentGetPropertyValueString(nullptr, "key", buf, sizeof(buf)) == kInvalidArgument, + "null intent data did not fail"); + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, nullptr, buf, sizeof(buf)) == kInvalidArgument, + "null key did not fail"); + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "", buf, sizeof(buf)) == kInvalidArgument, + "empty key did not fail"); + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "key", nullptr, sizeof(buf)) == kInvalidArgument, + "null value buf did not fail"); + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "key", buf, 0) == kInvalidArgument, + "zero buf size did not fail"); + + char canaryBuf[64]{}; + std::memset(canaryBuf, 'X', sizeof(canaryBuf)); + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "key", canaryBuf, 0) == kInvalidArgument, + "zero buf size canary call did not fail"); + Require(canaryBuf[0] == 'X', "buffer was modified on zero size error"); + Require(sceNpGameIntentGetPropertyValueString(nullptr, "key", canaryBuf, sizeof(canaryBuf)) == kInvalidArgument, + "null data canary call did not fail"); + Require(canaryBuf[0] == 'X', "buffer was modified on null data error"); + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, nullptr, canaryBuf, sizeof(canaryBuf)) == kInvalidArgument, + "null key canary call did not fail"); + Require(canaryBuf[0] == 'X', "buffer was modified on null key error"); + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "", canaryBuf, sizeof(canaryBuf)) == kInvalidArgument, + "empty key canary call did not fail"); + Require(canaryBuf[0] == 'X', "buffer was modified on empty key error"); + + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "key", canaryBuf, sizeof(canaryBuf)) == kValueNotFound, "get property value mismatch"); + Require(canaryBuf[0] == '\0', "valid call did not write terminator"); + + canaryBuf[0] = 'Y'; + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "key", canaryBuf, 1) == kValueNotFound, + "bufSize 1 did not succeed"); + Require(canaryBuf[0] == '\0', "bufSize 1 did not write terminator"); Require(sceNpGameIntentVshInitialize() == 0, "vsh init failed"); Require(sceNpGameIntentVshTerminate() == 0, "vsh terminate failed"); Require(sceNpGameIntentTerminate() == 0, "terminate failed"); + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "key", buf, sizeof(buf)) == kNotInitialized, + "post-terminate get property value must report not-initialized"); + Require(sceNpGameIntentReceiveIntent(&info) == kNotInitialized, + "post-terminate receive intent must report not-initialized"); + unsigned char appLauncherParam[0x28]{}; *reinterpret_cast(appLauncherParam) = 0x28; Require(sceNpAppLauncherInitialize(appLauncherParam) == 0, "app launcher init failed"); - Require(Throws([] { sceNpAppLauncherLaunchApp(); }), - "app launcher launch must throw until implemented"); Require(sceNpAppLauncherTerminate() == 0, "app launcher terminate failed"); *reinterpret_cast(appLauncherParam) = 0x10; Require(sceNpAppLauncherInitialize(appLauncherParam) == 0, diff --git a/core/libs/tests/GuestNpManager.cpp b/core/libs/tests/GuestNpManager.cpp index 43c844bb62..9f5113606d 100644 --- a/core/libs/tests/GuestNpManager.cpp +++ b/core/libs/tests/GuestNpManager.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -17,6 +18,11 @@ constexpr int UserNotFound = static_cast(0x80550007u); constexpr int AlreadyRegistered = static_cast(0x80550008u); constexpr int NotRegistered = static_cast(0x80550009u); constexpr int RequestNotFound = static_cast(0x80550014u); +constexpr int InvalidId = static_cast(0x80550015u); +constexpr int IdMapperRefusedArgument = static_cast(0x80553083u); +constexpr int IdMapperRefusedRequestId = static_cast(0x80553085u); +constexpr int ManagerIntTableFull = static_cast(0x80559D05u); +constexpr int ManagerIntNotInTable = static_cast(0x80559D0Au); void Require(bool condition, const char* message) { if (!condition) { @@ -112,8 +118,11 @@ void CheckStateCallbacksA() { Require(SignedOutOnce(stateCallsA[index], &userdata[index]), "each callback gets SIGNED_OUT once"); } Require(stateCallsA[8].calls == 0, "a rejected callback was called"); - Require(sceNpUnregisterStateCallbackA(0) == InvalidArgument, "callback id 0"); - Require(sceNpUnregisterStateCallbackA(9) == InvalidArgument, "callback id 9"); + Require(sceNpUnregisterStateCallbackA(0) == InvalidId, "callback id 0"); + Require(sceNpUnregisterStateCallbackA(9) == InvalidId, "callback id 9"); + Require(sceNpUnregisterStateCallbackA(-1) == InvalidId, "callback id -1"); + Require(sceNpUnregisterStateCallbackA(std::numeric_limits::min()) == InvalidId, "callback id INT_MIN"); + Require(sceNpUnregisterStateCallbackA(std::numeric_limits::max()) == InvalidId, "callback id INT_MAX"); Require(sceNpUnregisterStateCallbackA(ids[3]) == 0, "unregistering one state callback"); for (int index = 0; index < 8; ++index) { if (index == 3) continue; @@ -216,6 +225,73 @@ void CheckAsyncRequestLifecycle() { Require(sceNpDeleteRequest(unusedAsyncResult) == RequestNotFound, "second delete"); } +void CheckIdMapperRequests() { + for (const int reqId : {0, -1, -0x7fffffff}) { + Require(sceNpIdMapperAbortRequest(reqId) == IdMapperRefusedRequestId, "sceNpIdMapperAbortRequest must refuse a request id of 0 or less"); + Require(sceNpIdMapperDeleteRequest(reqId) == IdMapperRefusedRequestId, "sceNpIdMapperDeleteRequest must refuse a request id of 0 or less"); + } + const int first = sceNpIdMapperCreateRequest(); + const int second = sceNpIdMapperCreateRequest(); + Require(first > 0 && second > 0 && first != second, "sceNpIdMapperCreateRequest must return distinct positive request ids"); + Require(sceNpIdMapperAbortRequest(first) == 0, "abort on a live IdMapper request"); + Require(sceNpIdMapperAbortRequest(first) == 0, "abort must leave the IdMapper request live"); + Require(sceNpIdMapperDeleteRequest(first) == 0, "delete after abort on an IdMapper request"); + Require(sceNpIdMapperAbortRequest(first) == RequestNotFound, "abort after delete"); + Require(sceNpIdMapperDeleteRequest(first) == RequestNotFound, "second delete of an IdMapper request"); + Require(sceNpIdMapperAbortRequest(0x7fffffff) == RequestNotFound, "abort of a never-created IdMapper request"); + Require(sceNpIdMapperDeleteRequest(0x7fffffff) == RequestNotFound, "delete of a never-created IdMapper request"); + Require(sceNpIdMapperDeleteRequest(second) == 0, "delete the second IdMapper request"); +} + +void CheckManagerIntRequests() { + int live[64]; + for (int& reqId : live) { + reqId = sceNpManagerIntCreateRequest(); + Require(reqId > 0, "sceNpManagerIntCreateRequest must return a positive request id while a slot is free"); + } + for (std::size_t i = 0; i < 64; ++i) { + for (std::size_t j = i + 1; j < 64; ++j) Require(live[i] != live[j], "live NpManagerInt request ids must differ"); + } + Require(sceNpManagerIntCreateRequest() == ManagerIntTableFull, "the 65th live NpManagerInt request must give the full-table error"); + Require(sceNpManagerIntDeleteRequest(live[0]) == 0, "deleting a live NpManagerInt request"); + live[0] = sceNpManagerIntCreateRequest(); + Require(live[0] > 0, "a deleted NpManagerInt request frees its slot"); + Require(sceNpManagerIntCreateRequest() == ManagerIntTableFull, "the table is full again"); + Require(sceNpManagerIntAbortRequest(live[1]) == 0, "abort on a live NpManagerInt request"); + Require(sceNpManagerIntAbortRequest(live[1]) == 0, "abort must leave the NpManagerInt request live"); + Require(sceNpManagerIntDeleteRequest(live[1]) == 0, "delete after abort on an NpManagerInt request"); + Require(sceNpManagerIntDeleteRequest(live[1]) == ManagerIntNotInTable, "second delete of an NpManagerInt request"); + for (const int reqId : {0, -1, 0x7fffffff}) { + Require(sceNpManagerIntDeleteRequest(reqId) == ManagerIntNotInTable, "delete of an NpManagerInt request id not in the table"); + Require(sceNpManagerIntAbortRequest(reqId) == RequestNotFound, "abort of an unknown NpManagerInt request id"); + } + Require(sceNpManagerIntAbortRequest(live[1]) == RequestNotFound, "abort of a deleted NpManagerInt request id"); + for (std::size_t i = 0; i < 64; ++i) { + if (i != 1) Require(sceNpManagerIntDeleteRequest(live[i]) == 0, "deleting each NpManagerInt request"); + } +} + +void CheckIdMapperLookup(const char* name, decltype(&sceNpIdMapperNpIdToAccountId) lookup) { + constexpr int UserId = 0x10000000; + unsigned char input[36] = {}; + unsigned char output[64]; + std::memset(output, 0x5a, sizeof(output)); + unsigned char untouched[sizeof(output)]; + std::memcpy(untouched, output, sizeof(output)); + Require(lookup(0, UserId, input, output, 1, 0, 0) == IdMapperRefusedRequestId, name); + Require(lookup(-1, UserId, input, output, 1, 0, 0) == IdMapperRefusedRequestId, name); + Require(lookup(1, -1, input, output, 1, 0, 0) == IdMapperRefusedArgument, name); + Require(lookup(1, UserId, nullptr, output, 1, 0, 0) == IdMapperRefusedArgument, name); + Require(lookup(1, UserId, input, nullptr, 1, 0, 0) == IdMapperRefusedArgument, name); + Require(lookup(1, UserId, input, output, 0, 0, 0) == IdMapperRefusedArgument, name); + Require(lookup(1, UserId, input, output, 1001, 0, 0) == IdMapperRefusedArgument, name); + Require(lookup(1, UserId, input, output, 0xffffffffu, 0, 0) == IdMapperRefusedArgument, name); + Require(lookup(0, UserId, nullptr, output, 1, 0, 0) == IdMapperRefusedRequestId, name); + Require(lookup(1, UserId, input, output, 1, 0, 0) == SignedOut, name); + Require(lookup(0x7fffffff, UserId, input, output, 1000, 0xff, ~std::uint64_t{0}) == SignedOut, name); + Require(std::memcmp(output, untouched, sizeof(output)) == 0, name); +} + void CheckOnlineIdQueries() { NpOnlineId onlineId{}; std::memcpy(onlineId.data, "player", 6); @@ -374,6 +450,10 @@ int main() { CheckLegacyStateCallback(); CheckRequestTimeouts(); CheckAsyncRequestLifecycle(); + CheckIdMapperRequests(); + CheckManagerIntRequests(); + CheckIdMapperLookup("sceNpIdMapperNpIdToAccountId", sceNpIdMapperNpIdToAccountId); + CheckIdMapperLookup("sceNpIdMapperOnlineIdToAccountId", sceNpIdMapperOnlineIdToAccountId); CheckPlusAndContentRestriction(); NpId npId{}; std::memset(&npId, 0x5a, sizeof(npId)); diff --git a/core/libs/tests/GuestNpParty.cpp b/core/libs/tests/GuestNpParty.cpp index fbda80be4a..2ee253c2b5 100644 --- a/core/libs/tests/GuestNpParty.cpp +++ b/core/libs/tests/GuestNpParty.cpp @@ -1,4 +1,4 @@ -#include "prx/libSceNpParty/include/Api.hpp" +#include "prx/libSceNpParty/libSceNpParty.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -34,11 +34,14 @@ bool lifecycleTests() { return check(sceNpPartyTerminate() == kNotInitialized, "terminate before initialize did not fail") && check(sceNpPartyCheckCallback() == kNotInitialized, "check callback before initialize did not fail") && check(sceNpPartyRegisterHandlerA(handlers, nullptr) == kNotInitialized, "register handler before initialize did not fail") + && check(sceNpPartyRegisterHandler(handlers, nullptr) == kNotInitialized, "the plain register handler before initialize did not fail") && check(sceNpPartyInitialize(nullptr) == kInvalidArgument, "a null initialize param was not refused") && check(sceNpPartyInitialize(param) == 0, "initialize failed") && check(sceNpPartyInitialize(param) == kAlreadyInitialized, "a second initialize did not fail") && check(sceNpPartyRegisterHandlerA(nullptr, nullptr) == kInvalidArgument, "null handlers were not refused") && check(sceNpPartyRegisterHandlerA(handlers, param) == 0, "register handler failed") + && check(sceNpPartyRegisterHandler(nullptr, nullptr) == kInvalidArgument, "null plain handlers were not refused") + && check(sceNpPartyRegisterHandler(handlers, param) == 0, "the plain register handler failed") && check(sceNpPartyCheckCallback() == 0, "check callback failed") && check(sceNpPartyTerminate() == 0, "terminate failed") && check(sceNpPartyTerminate() == kNotInitialized, "a second terminate did not fail") @@ -55,6 +58,10 @@ bool notInPartyTests() { && check(sceNpPartyGetMembersA(out) == kNotInParty, "get members did not report NOT_IN_PARTY") && check(sceNpPartyGetMemberInfoA(1, nullptr) == kInvalidArgument, "a null member info was not refused") && check(sceNpPartyGetMemberInfoA(1, out) == kNotInParty, "get member info did not report NOT_IN_PARTY") + && check(sceNpPartyGetMembers(nullptr) == kInvalidArgument, "a null plain member list was not refused") + && check(sceNpPartyGetMembers(out) == kNotInParty, "the plain get members did not report NOT_IN_PARTY") + && check(sceNpPartyGetMemberInfo(1, nullptr) == kInvalidArgument, "a null plain member info was not refused") + && check(sceNpPartyGetMemberInfo(1, out) == kNotInParty, "the plain get member info did not report NOT_IN_PARTY") && check(sceNpPartyGetMemberSessionInfo(1, nullptr) == kInvalidArgument, "a null session info was not refused") && check(sceNpPartyGetMemberSessionInfo(1, out) == kNotInParty, "get session info did not report NOT_IN_PARTY") && check(sceNpPartyGetMemberVoiceInfo(1, 2, nullptr) == kInvalidArgument, "a null voice state was not refused") diff --git a/core/libs/tests/GuestNpPs4Imports.cpp b/core/libs/tests/GuestNpPs4Imports.cpp index b863dd65b5..4ffc79c2fe 100644 --- a/core/libs/tests/GuestNpPs4Imports.cpp +++ b/core/libs/tests/GuestNpPs4Imports.cpp @@ -31,17 +31,6 @@ void Require(bool condition, const char* message) { } } -template -void RequireNotImplemented(TCall call, const std::string& name) { - try { - call(); - } catch (const std::runtime_error& error) { - Require(std::string(error.what()) == name + " not implemented", "a declared function threw another error"); - return; - } - Require(false, "a declared function returned"); -} - void WriteBigEndian(std::vector& bytes, std::size_t at, std::uint64_t value, std::size_t width) { for (std::size_t index = 0; index < width; ++index) bytes[at + index] = static_cast(value >> (8u * (width - 1u - index))); } @@ -129,15 +118,10 @@ void CheckMatching2() { Require(sceNpMatching2Terminate() == 0, "NpMatching2 did not terminate after the SSL form"); } -void CheckSignaling() { - RequireNotImplemented([] { sceNpSignalingDeactivateConnection(); }, "sceNpSignalingDeactivateConnection"); -} - } int main() { CheckTrophyInfo(); CheckMatching2(); - CheckSignaling(); return 0; } diff --git a/core/libs/tests/GuestNpSignalingOffline.cpp b/core/libs/tests/GuestNpSignalingOffline.cpp index 4bc7234683..f457b0aeae 100644 --- a/core/libs/tests/GuestNpSignalingOffline.cpp +++ b/core/libs/tests/GuestNpSignalingOffline.cpp @@ -61,6 +61,7 @@ int main() { Require(sceNpSignalingSetContextOption(1, 1, 1) == NotInitialized, "sceNpSignalingSetContextOption", "a call before initialization did not return NOT_INITIALIZED"); Require(sceNpSignalingActivateConnection(1, nullptr, nullptr) == NotInitialized, "sceNpSignalingActivateConnection", "a call before initialization did not return NOT_INITIALIZED first"); Require(sceNpSignalingTerminateConnection(1, 1) == NotInitialized, "sceNpSignalingTerminateConnection", "a call before initialization did not return NOT_INITIALIZED"); + Require(sceNpSignalingDeactivateConnection(1, 1) == NotInitialized, "sceNpSignalingDeactivateConnection", "a call before initialization did not return NOT_INITIALIZED"); Require(sceNpSignalingGetConnectionInfo(1, 1, 1, nullptr) == NotInitialized, "sceNpSignalingGetConnectionInfo", "a call before initialization did not return NOT_INITIALIZED first"); Require(sceNpSignalingTerminate() == NotInitialized, "sceNpSignalingTerminate", "terminating before initialization did not return NOT_INITIALIZED"); @@ -104,6 +105,8 @@ int main() { Require(sceNpSignalingTerminateConnection(99, 1) == CtxNotFound, "sceNpSignalingTerminateConnection", "an unknown context was taken"); Require(sceNpSignalingTerminateConnection(context, 1) == ConnNotFound, "sceNpSignalingTerminateConnection", "a connection that was never created was found"); + Require(sceNpSignalingDeactivateConnection(99, 1) == CtxNotFound, "sceNpSignalingDeactivateConnection", "an unknown context was taken"); + Require(sceNpSignalingDeactivateConnection(context, 1) == ConnNotFound, "sceNpSignalingDeactivateConnection", "a connection that was never created was found"); Require(sceNpSignalingGetConnectionInfo(99, 1, 1, nullptr) == InvalidArgument, "sceNpSignalingGetConnectionInfo", "a null output was not refused before the context"); Require(sceNpSignalingGetConnectionInfo(99, 1, 1, info) == CtxNotFound, "sceNpSignalingGetConnectionInfo", "an unknown context was taken"); Require(sceNpSignalingGetConnectionInfo(context, 1, 1, info) == ConnNotFound, "sceNpSignalingGetConnectionInfo", "a connection that was never created was found"); diff --git a/core/libs/tests/GuestNpTrophy.cpp b/core/libs/tests/GuestNpTrophy.cpp index 51da08e5d2..768dd77112 100644 --- a/core/libs/tests/GuestNpTrophy.cpp +++ b/core/libs/tests/GuestNpTrophy.cpp @@ -14,6 +14,8 @@ namespace { constexpr int InvalidArgument = static_cast(0x80551604u); constexpr int InsufficientBuffer = static_cast(0x80551605u); +constexpr int Abort = static_cast(0x80551607u); +constexpr int InvalidHandle = static_cast(0x80551608u); constexpr int InvalidContext = static_cast(0x80551609u); constexpr int InvalidTrophyId = static_cast(0x8055160au); constexpr int AlreadyUnlocked = static_cast(0x8055160cu); @@ -97,8 +99,27 @@ int main() { Require(sceNpTrophyCreateContext(&missing, 1, 1, 0) == 0, "context for a label without a package"); Require(sceNpTrophyRegisterContext(missing, handle, 0) == TitleConfNotInstalled, "register without a trophy package"); + Require(sceNpTrophyCreateHandle(nullptr) == InvalidArgument, "null handle creation"); + Require(sceNpTrophyCreateContext(nullptr, 1, 0, 0) == InvalidArgument, "null context creation"); + Require(sceNpTrophyGetGameIcon(context, handle, icon, nullptr) == InvalidArgument, "null game icon size pointer"); + Require(sceNpTrophyGetGroupIcon(context, handle, 0, icon, nullptr) == InvalidArgument, "null group icon size pointer"); + Require(sceNpTrophyGetTrophyIcon(context, handle, 0, icon, nullptr) == InvalidArgument, "null trophy icon size pointer"); + Require(sceNpTrophyDestroyHandle(-1) == InvalidHandle, "invalid handle destruction"); Require(sceNpTrophyDestroyContext(missing) == 0, "destroy the second context"); + int live = 0; + Require(sceNpTrophyCreateHandle(&live) == 0, "second handle"); + Require(sceNpTrophyAbortHandle(handle) == 0, "abort a live handle"); + size = sizeof(icon); + Require(sceNpTrophyGetTrophyUnlockState(context, handle, reinterpret_cast(flags), &count) == Abort, "unlock state through an aborted handle"); + Require(sceNpTrophyGetGameIcon(context, handle, icon, &size) == Abort, "game icon through an aborted handle"); + Require(sceNpTrophyUnlockTrophy(context, handle, 0, &platinum) == Abort, "unlock through an aborted handle"); + Require(sceNpTrophyRegisterContext(context, handle, 0) == Abort, "register through an aborted handle"); + Require(sceNpTrophyAbortHandle(handle) == 0, "abort the handle again"); + Require(sceNpTrophyGetTrophyUnlockState(context, live, reinterpret_cast(flags), &count) == 0, "the other handle still works"); + Require(sceNpTrophyDestroyHandle(live) == 0, "destroy the second handle"); + Require(sceNpTrophyAbortHandle(999) == InvalidHandle && sceNpTrophyAbortHandle(-1) == InvalidHandle, "abort an unknown handle"); Require(sceNpTrophyDestroyHandle(handle) == 0 && sceNpTrophyDestroyContext(context) == 0, "destroy the handle and context"); + Require(sceNpTrophyAbortHandle(handle) == InvalidHandle, "abort a destroyed handle"); Require(sceNpTrophyDestroyContext(context) == InvalidContext, "destroy the context twice"); std::filesystem::current_path(root.parent_path()); diff --git a/core/libs/tests/GuestNpTrophyStats.cpp b/core/libs/tests/GuestNpTrophyStats.cpp index 0128a97160..35511ae5b6 100644 --- a/core/libs/tests/GuestNpTrophyStats.cpp +++ b/core/libs/tests/GuestNpTrophyStats.cpp @@ -45,6 +45,20 @@ static const char StatsDefinition[] = R"({"statDefinitionArray":[ {"statId":10,"dataType":"uint64","aggregation":"latest"},{"statId":20,"dataType":"uint64","aggregation":"latest"}, {"statId":30,"dataType":"uint32","aggregation":"latest"},{"statId":40,"dataType":"uint32","aggregation":"latest"}]})"; +static const char ProgressConf[] = R"({"defaultLanguage":"en-US","trophies":[ + {"id":"0","grade":"P"}, + {"id":"1","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"112","comparator":"ge","targetValue":"10","progressive":true}}, + {"id":"2","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"113","comparator":"ge","targetValue":"3","progressive":true}} +]})"; + +static const char ProgressDefinition[] = R"({"statDefinitionArray":[ + {"statId":112,"dataType":"int32","aggregation":"max","minValue":"0","initialValue":"0"}, + {"statId":113,"dataType":"int32","aggregation":"max","minValue":"0","initialValue":"0"}]})"; + +static const char ProgressExtraction[] = R"({"statsExtractionRuleArray":[ + {"condition":{"eventName":"_UpdateTrophyProgress","property":{"path":"$._trophy_id","comparator":"==","value":"1"}},"action":{"input":"$._trophy_progress","output":{"statId":112}}}, + {"condition":{"eventName":"_UpdateTrophyProgress","property":{"path":"$._trophy_id","comparator":"==","value":"2"}},"action":{"input":"$._trophy_progress","output":{"statId":113}}}]})"; + static const char SumDefinition[] = R"({"statDefinitionArray":[{"statId":10,"dataType":"uint64","aggregation":"sum"}]})"; static const char StatsExtraction[] = R"({"statsExtractionRuleArray":[ @@ -308,11 +322,36 @@ static void IgnoreOtherAggregations(Unlocks& unlocks) { Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "counter", 10) == 0, "counter was not set"); }); const auto log = capture.Finish(); - Require(log.find("[trophy] /app0/sce_sys/uds/uds05.ucp: stat 10 has aggregation \"sum\"; only \"latest\" is supported") != std::string::npos, + Require(log.find("[trophy] /app0/sce_sys/uds/uds05.ucp: stat 10 has aggregation \"sum\"; only \"latest\" and \"max\" are supported") != std::string::npos, "a stat with a sum aggregation was not reported"); Require(!Trophy(context, 1).unlocked && !Exists("_trophy/268435456_5.stats.txt") && unlocks.trophies.size() == 6, "a stat with a sum aggregation was stored or unlocked a trophy"); } +static void RecordTrophyProgress(Unlocks& unlocks) { + const auto context = TrophyContext(6); + const auto uds = OpenUds(6); + const auto progress = [&uds](std::int32_t trophy, std::int32_t value) { + Post(uds, "_UpdateTrophyProgress", [trophy, value](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetInt32(properties, "_trophy_id", trophy) == 0, "_trophy_id was not set"); + Require(sceNpUniversalDataSystemEventPropertyObjectSetInt32(properties, "_trophy_progress", value) == 0, "_trophy_progress was not set"); + }); + }; + progress(1, 4); + const auto partway = Trophy(context, 1); + Require(!partway.unlocked && partway.progress.type == 1 && partway.progress.value == 4 && Trophy(context, 2).progress.value == 0, "trophy progress was not recorded for its own trophy"); + progress(1, 2); + Require(Trophy(context, 1).progress.value == 4, "lower trophy progress replaced higher progress"); + progress(2, 3); + Require(Trophy(context, 2).unlocked && !Trophy(context, 1).unlocked && !Trophy(context, 0).unlocked, "trophy progress at its target did not unlock only that trophy"); + progress(1, 12); + const auto done = Trophy(context, 1); + Require(done.unlocked && done.progress.value == 10 && Trophy(context, 0).unlocked, "trophy progress past its target did not unlock the trophy and the platinum"); + Require(Read("_trophy/268435456_6.stats.txt") == "112 12\n113 3\n", "trophy progress was not stored"); + const std::vector> expected{{6, 2}, {6, 1}, {6, 0}}; + Require(unlocks.trophies.size() == 9 && std::vector>(unlocks.trophies.end() - 3, unlocks.trophies.end()) == expected, + "the listener did not see the progress unlocks and the platinum once, in order"); +} + int main() { const auto previous = std::filesystem::current_path(); const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-stats-" + std::to_string(std::random_device{}())); @@ -329,6 +368,8 @@ int main() { Write("app0/sce_sys/uds/uds03.ucp", rules.substr(0, rules.size() - 1)); Write("app0/sce_sys/trophy2/trophy05.ucp", trophies); Write("app0/sce_sys/uds/uds05.ucp", Ucp({{"stats_definition.json", SumDefinition}, {"stats_extraction.json", StatsExtraction}})); + Write("app0/sce_sys/trophy2/trophy06.ucp", Ucp({{"tropconf.json", ProgressConf}, {"tropmeta_en-US.json", TropMeta}})); + Write("app0/sce_sys/uds/uds06.ucp", Ucp({{"stats_definition.json", ProgressDefinition}, {"stats_extraction.json", ProgressExtraction}})); Write("_trophy/268435456_0.stats.txt", "40 2\n"); Unlocks unlocks; @@ -338,6 +379,7 @@ int main() { UnlockThroughStats(context, unlocks); IgnoreMissingAndMalformedFiles(unlocks); IgnoreOtherAggregations(unlocks); + RecordTrophyProgress(unlocks); NpTrophy2RemoveUnlockListener_nid_no_patch(listener); std::filesystem::current_path(previous); diff --git a/core/libs/tests/GuestNpTusOffline.cpp b/core/libs/tests/GuestNpTusOffline.cpp index 565dcceaa4..d4157cb607 100644 --- a/core/libs/tests/GuestNpTusOffline.cpp +++ b/core/libs/tests/GuestNpTusOffline.cpp @@ -1,4 +1,4 @@ -#include "prx/libSceNpTus/include/Api.hpp" +#include "prx/libSceNpTus/libSceNpTus.hpp" #include "SceTypes.hpp" #include #include @@ -24,6 +24,7 @@ NpOnlineId emptyUser{}; unsigned char npIds[2][64]; std::uint64_t accounts[2] = {1, 2}; std::int32_t slots[2] = {1, 2}; +std::int64_t variables[2] = {5, 6}; std::int32_t manySlots[65] = {}; std::uint64_t length = 0; int failures = 0; @@ -128,9 +129,11 @@ int main() { CheckSync("sceNpTusAddAndGetVariableVUser", [&](int req) { return sceNpTusAddAndGetVariableVUser(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); CheckSync("sceNpTusDeleteMultiSlotData", [&](int req) { return sceNpTusDeleteMultiSlotData(req, &npId, slots, 2, nullptr); }); CheckSync("sceNpTusDeleteMultiSlotDataA", [&](int req) { return sceNpTusDeleteMultiSlotDataA(req, 1, slots, 2, nullptr); }); + CheckSync("sceNpTusGetData", [&](int req) { return sceNpTusGetData(req, &npId, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }); CheckSync("sceNpTusGetDataA", [&](int req) { return sceNpTusGetDataA(req, 1, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }); CheckSync("sceNpTusGetDataAVUser", [&](int req) { return sceNpTusGetDataAVUser(req, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }); CheckSync("sceNpTusGetDataForCrossSave", [&](int req) { return sceNpTusGetDataForCrossSave(req, 1, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }); + CheckSync("sceNpTusGetDataVUser", [&](int req) { return sceNpTusGetDataVUser(req, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }); CheckSync("sceNpTusGetFriendsDataStatus", [&](int req) { return sceNpTusGetFriendsDataStatus(req, 1, 0, 1, buffer, sizeof(buffer), 2, nullptr); }); CheckSync("sceNpTusGetFriendsVariable", [&](int req) { return sceNpTusGetFriendsVariable(req, 1, 0, 1, buffer, sizeof(buffer), 2, nullptr); }); CheckSync("sceNpTusGetMultiSlotDataStatus", [&](int req) { return sceNpTusGetMultiSlotDataStatus(req, &npId, slots, buffer, sizeof(buffer), 2, nullptr); }); @@ -139,6 +142,8 @@ int main() { CheckSync("sceNpTusGetMultiSlotDataStatusForCrossSave", [&](int req) { return sceNpTusGetMultiSlotDataStatusForCrossSave(req, 1, slots, buffer, sizeof(buffer), 2, nullptr); }); CheckSync("sceNpTusGetMultiSlotDataStatusForCrossSaveVUser", [&](int req) { return sceNpTusGetMultiSlotDataStatusForCrossSaveVUser(req, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }); CheckSync("sceNpTusGetMultiSlotDataStatusVUser", [&](int req) { return sceNpTusGetMultiSlotDataStatusVUser(req, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiSlotVariable", [&](int req) { return sceNpTusGetMultiSlotVariable(req, &npId, slots, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiSlotVariableVUser", [&](int req) { return sceNpTusGetMultiSlotVariableVUser(req, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }); CheckSync("sceNpTusGetMultiUserDataStatus", [&](int req) { return sceNpTusGetMultiUserDataStatus(req, npIds, 1, buffer, sizeof(buffer), 2, nullptr); }); CheckSync("sceNpTusGetMultiUserDataStatusA", [&](int req) { return sceNpTusGetMultiUserDataStatusA(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }); CheckSync("sceNpTusGetMultiUserDataStatusAVUser", [&](int req) { return sceNpTusGetMultiUserDataStatusAVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); @@ -148,16 +153,27 @@ int main() { CheckSync("sceNpTusGetMultiUserVariable", [&](int req) { return sceNpTusGetMultiUserVariable(req, npIds, 1, buffer, sizeof(buffer), 2, nullptr); }); CheckSync("sceNpTusGetMultiUserVariableA", [&](int req) { return sceNpTusGetMultiUserVariableA(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }); CheckSync("sceNpTusGetMultiUserVariableAVUser", [&](int req) { return sceNpTusGetMultiUserVariableAVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserVariableForCrossSave", [&](int req) { return sceNpTusGetMultiUserVariableForCrossSave(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }); CheckSync("sceNpTusGetMultiUserVariableForCrossSaveVUser", [&](int req) { return sceNpTusGetMultiUserVariableForCrossSaveVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); CheckSync("sceNpTusGetMultiUserVariableVUser", [&](int req) { return sceNpTusGetMultiUserVariableVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusSetData", [&](int req) { return sceNpTusSetData(req, &npId, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }); CheckSync("sceNpTusSetDataA", [&](int req) { return sceNpTusSetDataA(req, 1, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }); CheckSync("sceNpTusSetDataAVUser", [&](int req) { return sceNpTusSetDataAVUser(req, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }); + CheckSync("sceNpTusSetDataVUser", [&](int req) { return sceNpTusSetDataVUser(req, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }); + CheckSync("sceNpTusSetMultiSlotVariable", [&](int req) { return sceNpTusSetMultiSlotVariable(req, &npId, slots, variables, 2, nullptr); }); + CheckSync("sceNpTusSetMultiSlotVariableVUser", [&](int req) { return sceNpTusSetMultiSlotVariableVUser(req, &virtualUser, slots, variables, 2, nullptr); }); CheckSync("sceNpTusTryAndSetVariable", [&](int req) { return sceNpTusTryAndSetVariable(req, &npId, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); CheckSync("sceNpTusTryAndSetVariableA", [&](int req) { return sceNpTusTryAndSetVariableA(req, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); CheckSync("sceNpTusTryAndSetVariableAVUser", [&](int req) { return sceNpTusTryAndSetVariableAVUser(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); CheckSync("sceNpTusTryAndSetVariableForCrossSave", [&](int req) { return sceNpTusTryAndSetVariableForCrossSave(req, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); CheckSync("sceNpTusTryAndSetVariableForCrossSaveVUser", [&](int req) { return sceNpTusTryAndSetVariableForCrossSaveVUser(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); CheckSync("sceNpTusTryAndSetVariableVUser", [&](int req) { return sceNpTusTryAndSetVariableVUser(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + { + const int req = NewRequest(); + Require(sceNpTusGetData(req, &npId, 1, buffer, sizeof(buffer), buffer, 16, &buffer) == InvalidArgument, "sceNpTusGetData", "an option was not refused before waiting"); + Require(sceNpTusSetMultiSlotVariable(req, &npId, manySlots, reinterpret_cast(buffer), 65, nullptr) == TooManySlotId, "sceNpTusSetMultiSlotVariable", "65 slots were not refused before waiting"); + sceNpTusDeleteRequest(req); + } { int req = NewRequest(); Require(sceNpTusAddAndGetVariableVUserAsync(req, &emptyUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr) == InvalidArgument, "sceNpTusAddAndGetVariableVUserAsync", "an empty virtual user was taken"); diff --git a/core/libs/tests/GuestNpUtilityOffline.cpp b/core/libs/tests/GuestNpUtilityOffline.cpp index 42c76ec96c..7f92da4f61 100644 --- a/core/libs/tests/GuestNpUtilityOffline.cpp +++ b/core/libs/tests/GuestNpUtilityOffline.cpp @@ -3,6 +3,9 @@ #include #include #include +#include +#include +#include namespace { @@ -13,6 +16,7 @@ constexpr int Aborted = static_cast(0x80550707u); constexpr int InsufficientArgument = static_cast(0x8055070Cu); constexpr int InvalidId = static_cast(0x8055070Eu); constexpr int InvalidType = static_cast(0x80550711u); +constexpr int LookupSynchronousRequest = static_cast(0x80550307u); constexpr int PollFinished = 0; constexpr int PollRunning = 1; constexpr int WordFilterTitleCtxBase = 0x01000000; @@ -116,13 +120,124 @@ void CheckWordFilter() { Require(sceNpLookupDeleteTitleCtx(context) == InvalidId, "sceNpLookupDeleteTitleCtx", "a word filter context was found among the lookup contexts"); } +void CheckBandwidthTestInitStart() { + Require(sceNpBandwidthTestInitStart() == SignedOut, "sceNpBandwidthTestInitStart", "starting a test while signed out did not return SIGNED_OUT"); + Require(sceNpBandwidthTestInitStart() == sceNpBandwidthTestInitStartDownload(nullptr), "sceNpBandwidthTestInitStart", "the answer differs from sceNpBandwidthTestInitStartDownload"); + Require(sceNpBandwidthTestInitStart() == sceNpBandwidthTestInitStartUpload(nullptr), "sceNpBandwidthTestInitStart", "the answer differs from sceNpBandwidthTestInitStartUpload"); +} + +void CheckBandwidthTestShutdownWithDetailedInfo() { + constexpr int npInvalidArgument = static_cast(0x80550003u); + Require(sceNpBandwidthTestShutdownWithDetailedInfo() == npInvalidArgument, "sceNpBandwidthTestShutdownWithDetailedInfo", "shutting down a test that never started did not return INVALID_ARGUMENT"); + Require(sceNpBandwidthTestShutdownWithDetailedInfo() == sceNpBandwidthTestShutdown(0, nullptr), "sceNpBandwidthTestShutdownWithDetailedInfo", "the answer differs from sceNpBandwidthTestShutdown"); +} + +} + +int WaitInThread(const char* name, int (APS5_VABI *wait)(int, int*), int reqId, int* result, void (*finish)(int)) { + int status = 7; + std::atomic entered{false}; + std::atomic returned{false}; + std::thread waiter([&] { + entered = true; + status = wait(reqId, result); + returned = true; + }); + while (!entered) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(!returned, name, "the wait returned before the request finished"); + finish(reqId); + waiter.join(); + return status; +} + +void StartLookup(int reqId) { + NpOnlineId onlineId{}; + std::strcpy(onlineId.data, "player_2"); + sceNpLookupNpId(reqId, &onlineId, npId, nullptr); +} + +void AbortLookup(int reqId) { + sceNpLookupAbortRequest(reqId); +} + +void DeleteLookup(int reqId) { + sceNpLookupDeleteRequest(reqId); +} + +void CheckLookupAsync() { + const int context = sceNpLookupCreateTitleCtx(selfNpId); + Require(sceNpLookupCreateAsyncRequest(context, nullptr) == InvalidArgument, "sceNpLookupCreateAsyncRequest", "a null parameter was taken"); + Require(sceNpLookupCreateAsyncRequest(context, wrongAsyncParam) == InvalidArgument, "sceNpLookupCreateAsyncRequest", "a parameter size other than 0x18 was taken"); + Require(sceNpLookupCreateAsyncRequest(context + 100, asyncParam) == InvalidId, "sceNpLookupCreateAsyncRequest", "an unknown context was taken"); + + Require(sceNpLookupSetTimeout(context, -1, 0, 0, 0, 0) == InvalidArgument, "sceNpLookupSetTimeout", "a negative retry count was taken"); + Require(sceNpLookupSetTimeout(context, 0, 0, 0, 0, 0) == InvalidArgument, "sceNpLookupSetTimeout", "settings without any effect were taken"); + Require(sceNpLookupSetTimeout(context, 0, 999999, 0, 0, 0) == InvalidArgument, "sceNpLookupSetTimeout", "a resolve timeout under 1 s was taken"); + Require(sceNpLookupSetTimeout(context, 0, 0, 9999999, 0, 0) == InvalidArgument, "sceNpLookupSetTimeout", "a connect timeout under 10 s was taken"); + Require(sceNpLookupSetTimeout(context, 0, 0, 0, 9999999, 0) == InvalidArgument, "sceNpLookupSetTimeout", "a send timeout under 10 s was taken"); + Require(sceNpLookupSetTimeout(context, 0, 0, 0, 0, 9999999) == InvalidArgument, "sceNpLookupSetTimeout", "a receive timeout under 10 s was taken"); + Require(sceNpLookupSetTimeout(context + 100, 1, 0, 0, 0, 0) == InvalidId, "sceNpLookupSetTimeout", "an unknown id was taken"); + Require(sceNpLookupSetTimeout(context, 1, 0, 0, 0, 0) == 0, "sceNpLookupSetTimeout", "a context timeout was refused"); + + const int async = sceNpLookupCreateAsyncRequest(context, asyncParam); + Require(async > 0, "sceNpLookupCreateAsyncRequest", "creating an asynchronous request failed"); + Require(sceNpLookupSetTimeout(async, 0, 1000000, 10000000, 10000000, 10000000) == 0, "sceNpLookupSetTimeout", "a request timeout was refused"); + int result = 7; + Require(sceNpLookupPollAsync(async + 100, &result) == InvalidId, "sceNpLookupPollAsync", "an unknown request was taken"); + Require(sceNpLookupPollAsync(async, nullptr) == InvalidArgument, "sceNpLookupPollAsync", "a null result was taken"); + Require(sceNpLookupPollAsync(async + 100, nullptr) == InvalidArgument, "sceNpLookupPollAsync", "an unknown request was checked before a null result"); + Require(sceNpLookupWaitAsync(async, nullptr) == InvalidArgument, "sceNpLookupWaitAsync", "a null result was taken"); + Require(sceNpLookupPollAsync(async, &result) == PollRunning && result == 7, "sceNpLookupPollAsync", "an unstarted request was not running"); + Require(WaitInThread("sceNpLookupWaitAsync", sceNpLookupWaitAsync, async, &result, StartLookup) == 0 && result == SignedOut, "sceNpLookupWaitAsync", "waiting did not end with SIGNED_OUT when the lookup started"); + Require(sceNpLookupPollAsync(async, nullptr) == InvalidArgument, "sceNpLookupPollAsync", "a null result was taken on a finished request"); + result = 7; + Require(sceNpLookupPollAsync(async, &result) == PollFinished && result == SignedOut, "sceNpLookupPollAsync", "the request did not finish with SIGNED_OUT"); + + const int sync = sceNpLookupCreateRequest(context); + StartLookup(sync); + result = 7; + Require(sceNpLookupWaitAsync(sync, &result) == LookupSynchronousRequest && result == 7, "sceNpLookupWaitAsync", "a synchronous request was not refused with 0x80550307"); + Require(sceNpLookupPollAsync(sync, &result) == LookupSynchronousRequest && result == 7, "sceNpLookupPollAsync", "a synchronous request was not refused with 0x80550307"); + + const int aborted = sceNpLookupCreateAsyncRequest(context, asyncParam); + Require(sceNpLookupAbortRequest(aborted + 100) == InvalidId, "sceNpLookupAbortRequest", "an unknown request was taken"); + Require(WaitInThread("sceNpLookupWaitAsync", sceNpLookupWaitAsync, aborted, &result, AbortLookup) == 0 && result == Aborted, "sceNpLookupAbortRequest", "waiting did not end with ABORTED"); + Require(sceNpLookupNpId(aborted, nullptr, npId, nullptr) == InsufficientArgument, "sceNpLookupNpId", "arguments were not checked before the abort"); + + const int deleted = sceNpLookupCreateAsyncRequest(context, asyncParam); + Require(WaitInThread("sceNpLookupWaitAsync", sceNpLookupWaitAsync, deleted, &result, DeleteLookup) == InvalidId, "sceNpLookupWaitAsync", "waiting on a deleted request did not return INVALID_ID"); + Require(sceNpLookupWaitAsync(deleted, &result) == InvalidId, "sceNpLookupWaitAsync", "an unknown request was taken"); + sceNpLookupDeleteTitleCtx(context); +} + +void CheckWordFilterAsync() { + const int context = sceNpWordFilterCreateTitleCtx(selfNpId); + Require(sceNpWordFilterSetTimeout(context, 0, 0, 0, 0, 0) == InvalidArgument, "sceNpWordFilterSetTimeout", "settings without any effect were taken"); + Require(sceNpWordFilterSetTimeout(context, 0, 0, 0, 0, 9999999) == InvalidArgument, "sceNpWordFilterSetTimeout", "a receive timeout under 10 s was taken"); + Require(sceNpWordFilterSetTimeout(context, 1, 0, 0, 0, 0) == 0, "sceNpWordFilterSetTimeout", "a context timeout was refused"); + Require(sceNpWordFilterSetTimeout(1, 1, 0, 0, 0, 0) == InvalidId, "sceNpWordFilterSetTimeout", "a lookup id was taken"); + const int sync = sceNpWordFilterCreateRequest(context); + const int async = sceNpWordFilterCreateAsyncRequest(context, asyncParam); + Require(sceNpWordFilterSetTimeout(async, 2, 1000000, 0, 0, 0) == 0, "sceNpWordFilterSetTimeout", "a request timeout was refused"); + int result = 7; + Require(sceNpWordFilterWaitAsync(async, nullptr) == InvalidArgument, "sceNpWordFilterWaitAsync", "a null result was taken"); + Require(sceNpWordFilterWaitAsync(sync, &result) == InvalidId, "sceNpWordFilterWaitAsync", "a synchronous request was taken"); + Require(sceNpWordFilterWaitAsync(async + 100, &result) == InvalidId, "sceNpWordFilterWaitAsync", "an unknown request was taken"); + Require(sceNpWordFilterCensorComment(async, "word", nullptr) == 0, "sceNpWordFilterCensorComment", "the asynchronous request did not start"); + Require(sceNpWordFilterWaitAsync(async, &result) == 0 && result == SignedOut, "sceNpWordFilterWaitAsync", "the request did not finish with SIGNED_OUT"); + sceNpWordFilterDeleteTitleCtx(context); } int main() { std::memset(selfNpId, 0, sizeof(selfNpId)); std::strcpy(reinterpret_cast(selfNpId), "player_1"); + CheckBandwidthTestInitStart(); + CheckBandwidthTestShutdownWithDetailedInfo(); CheckLookup(); CheckWordFilter(); + CheckLookupAsync(); + CheckWordFilterAsync(); if (failures != 0) return 1; std::puts("NpUtility offline tests passed"); return 0; diff --git a/core/libs/tests/GuestNpWebApi.cpp b/core/libs/tests/GuestNpWebApi.cpp index 55fc787258..465ee0900c 100644 --- a/core/libs/tests/GuestNpWebApi.cpp +++ b/core/libs/tests/GuestNpWebApi.cpp @@ -1,18 +1,40 @@ +#include "ProcessParametersFixture.hpp" #include "prx/libSceNpWebApi/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include +#include #include #include +#include namespace { constexpr int InvalidArgument = static_cast(0x80552902u); +constexpr int InvalidLibContextId = static_cast(0x80552903u); constexpr int LibContextNotFound = static_cast(0x80552904u); +constexpr int UserContextNotFound = static_cast(0x80552905u); +constexpr int RequestNotFound = static_cast(0x80552906u); +constexpr int NotSignedIn = static_cast(0x80552907u); +constexpr int InvalidContentParameter = static_cast(0x80552908u); +constexpr int Aborted = static_cast(0x80552909u); +constexpr int UserContextAlreadyExist = static_cast(0x8055290Au); constexpr int PushEventFilterNotFound = static_cast(0x8055290Bu); +constexpr int PushEventCallbackNotFound = static_cast(0x8055290Cu); constexpr int HandleNotFound = static_cast(0x8055290Du); constexpr int ServicePushEventFilterNotFound = static_cast(0x8055290Eu); +constexpr int ServicePushEventCallbackNotFound = static_cast(0x8055290Fu); constexpr int SignedInUserNotFound = static_cast(0x80552910u); +constexpr int MethodGet = 0; +constexpr int MethodPost = 1; +constexpr int MethodPatch = 4; + +alignas(16) std::array process{}; +const char onlineId[20] = "player"; +const char filterParam[16] = {}; +const char body[] = "{}"; + void Require(bool condition, const char* message) { if (!condition) { std::fprintf(stderr, "NpWebApi: %s\n", message); @@ -20,9 +42,26 @@ void Require(bool condition, const char* message) { } } +void APS5_VABI PushEvent() { } -int main() { +void* PushEventCallback() { + return reinterpret_cast(&PushEvent); +} + +void SetSdkVersion(std::uint32_t version) { + std::memcpy(process.data() + 16, &version, sizeof(version)); +} + +void InstallProcessParameters() { + const std::uint64_t size = process.size(); + const std::uint32_t magic = 0x4942524f; + std::memcpy(process.data(), &size, sizeof(size)); + std::memcpy(process.data() + 8, &magic, sizeof(magic)); + Require(ProcessParametersFixture::Install(process.data(), process.size()), "process parameters were not installed"); +} + +void TestLibraryContexts() { const int context = sceNpWebApiInitialize(1, 0x10000); Require(context > 0 && context <= 0x7fff, "library context id out of range"); const int other = sceNpWebApiInitialize(1, 0x10000); @@ -31,7 +70,6 @@ int main() { const int handle = sceNpWebApiCreateHandle(context); Require(handle > 0 && static_cast(handle) < 0xf0000000u, "handle id out of range"); - const char filterParam[16] = {}; Require(sceNpWebApiCreatePushEventFilter(context, nullptr, 1) == InvalidArgument, "push event filter without its parameter"); Require(sceNpWebApiCreatePushEventFilter(context, filterParam, 0) == InvalidArgument, "push event filter with no parameters"); const int filter = sceNpWebApiCreatePushEventFilter(context, filterParam, 1); @@ -40,6 +78,8 @@ int main() { Require(sceNpWebApiCreateServicePushEventFilter(context, handle, nullptr, 0, filterParam, 1) == InvalidArgument, "service filter without a service name"); Require(sceNpWebApiCreateServicePushEventFilter(0x7fff + 1, handle, "service", 0, filterParam, 1) == LibContextNotFound, "service filter of an unknown context"); Require(sceNpWebApiCreateServicePushEventFilter(other, handle, "service", 0, filterParam, 1) == HandleNotFound, "service filter with another context's handle"); + Require(sceNpWebApiCreateServicePushEventFilter(context, handle, "service", 0xffffffffu, filterParam, 1) == InvalidArgument, "service filter with the invalid service label from SDK 2.00"); + Require(sceNpWebApiCreateServicePushEventFilter(0x7fff + 1, handle, "service", 0xffffffffu, filterParam, 1) == InvalidArgument, "the service label is checked before the library context"); Require(sceNpWebApiCreateServicePushEventFilter(context, handle, "service", 0, filterParam, 1) == SignedInUserNotFound, "service filter without a signed-in user"); Require(sceNpWebApiDeleteServicePushEventFilter(context, 1) == ServicePushEventFilterNotFound, "no service filter exists"); @@ -53,5 +93,160 @@ int main() { Require(sceNpWebApiTerminate(context) == LibContextNotFound, "library context terminated twice"); Require(sceNpWebApiCreateHandle(context) == LibContextNotFound, "handle of a terminated context"); Require(sceNpWebApiTerminate(other) == 0, "terminate the second library context"); +} + +void TestUserContexts() { + const int context = sceNpWebApiInitialize(1, 0x10000); + const int other = sceNpWebApiInitialize(1, 0x10000); + Require(context > 0 && other > 0, "library contexts for user contexts"); + Require(sceNpWebApiTerminate(other) == 0, "terminate the unused library context"); + + Require(sceNpWebApiCreateContext(0x8000, onlineId) == InvalidLibContextId, "user context of a library context id above the range"); + Require(sceNpWebApiCreateContext(0x8000, nullptr) == InvalidLibContextId, "the library context id is checked before the online id"); + Require(sceNpWebApiCreateContext(context, nullptr) == InvalidArgument, "user context without an online id"); + Require(sceNpWebApiCreateContext(0, onlineId) == LibContextNotFound, "user context of library context 0"); + Require(sceNpWebApiCreateContext(other, onlineId) == LibContextNotFound, "user context of a terminated library context"); + + const int user = sceNpWebApiCreateContext(context, onlineId); + Require(user > 0 && (user >> 16) == context && (user & 0xffff) != 0, "user context id holds its library context"); + Require(sceNpWebApiCreateContext(context, onlineId) == UserContextAlreadyExist, "a second user context for the same user"); + + Require(sceNpWebApiDeleteContext(user + 1) == UserContextNotFound, "delete an unknown user context"); + Require(sceNpWebApiDeleteContext(-1) == LibContextNotFound, "delete user context -1"); + Require(sceNpWebApiDeleteContext((other << 16) | 1) == LibContextNotFound, "delete a user context of a terminated library context"); + Require(sceNpWebApiDeleteContext(user) == 0, "delete the user context"); + Require(sceNpWebApiDeleteContext(user) == UserContextNotFound, "user context deleted twice"); + + const int next = sceNpWebApiCreateContext(context, onlineId); + Require(next > 0 && next != user && (next >> 16) == context, "a new user context after the first was deleted"); + std::int64_t request = 0; + Require(sceNpWebApiCreateRequest(next, "group", "/path", MethodGet, nullptr, &request) == 0, "request before termination"); + Require(sceNpWebApiTerminate(context) == 0, "terminate a library context with a user context"); + Require(sceNpWebApiDeleteContext(next) == LibContextNotFound, "user context of a terminated library context"); + Require(sceNpWebApiSendRequest(request, nullptr, 0) == LibContextNotFound, "request of a terminated library context"); +} + +void TestRequests() { + const int context = sceNpWebApiInitialize(1, 0x10000); + const int user = sceNpWebApiCreateContext(context, onlineId); + Require(user > 0, "user context for requests"); + + std::int64_t request = 0; + Require(sceNpWebApiCreateRequest(user, nullptr, "/path", MethodGet, nullptr, &request) == InvalidArgument, "request without an API group"); + Require(sceNpWebApiCreateRequest(user, "group", nullptr, MethodGet, nullptr, &request) == InvalidArgument, "request without a path"); + const NpWebApiContentParameter untyped{sizeof(body), nullptr, {}}; + Require(sceNpWebApiCreateRequest(user, "group", "/path", MethodPost, &untyped, &request) == InvalidContentParameter, "content without a type"); + Require(sceNpWebApiCreateRequest(0, "group", "/path", MethodPost, &untyped, &request) == InvalidContentParameter, "content is checked before the user context"); + Require(sceNpWebApiCreateRequest(user, "group", "/path", MethodPatch, nullptr, &request) == InvalidArgument, "PATCH with SDK 2.50 or later"); + Require(sceNpWebApiCreateRequest(user + 1, "group", "/path", MethodGet, nullptr, &request) == UserContextNotFound, "request of an unknown user context"); + Require(sceNpWebApiCreateRequest(0, "group", "/path", MethodGet, nullptr, &request) == LibContextNotFound, "request of user context 0"); + Require(sceNpWebApiCreateRequest(-1, "group", "/path", MethodGet, nullptr, &request) == LibContextNotFound, "request of user context -1"); + Require(request == 0, "a failed request leaves the id alone"); + + const NpWebApiContentParameter empty{0, nullptr, {}}; + Require(sceNpWebApiCreateRequest(user, "group", "/path", MethodGet, &empty, nullptr) == 0, "empty content without a type and no id output"); + const NpWebApiContentParameter typed{sizeof(body), "application/json", {}}; + Require(sceNpWebApiCreateRequest(user, "group", "/path", MethodPost, &typed, &request) == 0, "create a request"); + Require(static_cast(request >> 32) == user && (request & 0xffffffff) != 0, "request id holds its user context"); + std::int64_t second = 0; + Require(sceNpWebApiCreateRequest(user, "group", "/path", MethodGet, nullptr, &second) == 0 && second != request && static_cast(second >> 32) == user, "a second request"); + + Require(sceNpWebApiSendRequest(request, body, sizeof(body)) == NotSignedIn, "a request cannot be sent without a signed-in user"); + Require(sceNpWebApiSendRequest(request, body, sizeof(body)) == NotSignedIn, "a failed send can be retried"); + Require(sceNpWebApiSendRequest(request + 0x10000, nullptr, 0) == RequestNotFound, "send an unknown request"); + Require(sceNpWebApiSendRequest(static_cast(user + 1) << 32 | 1, nullptr, 0) == UserContextNotFound, "send a request of an unknown user context"); + Require(sceNpWebApiSendRequest(1, nullptr, 0) == LibContextNotFound, "send a request of library context 0"); + + Require(sceNpWebApiAbortRequest(request + 0x10000) == RequestNotFound, "abort an unknown request"); + Require(sceNpWebApiAbortRequest(-1) == LibContextNotFound, "abort request -1"); + Require(sceNpWebApiDeleteRequest(-1) == LibContextNotFound, "delete request -1"); + Require(sceNpWebApiAbortRequest(request) == 0, "abort the request"); + Require(sceNpWebApiAbortRequest(request) == 0, "abort the request twice"); + Require(sceNpWebApiSendRequest(request, body, sizeof(body)) == Aborted, "send an aborted request"); + Require(sceNpWebApiSendRequest(second, nullptr, 0) == NotSignedIn, "aborting one request leaves the other"); + + Require(sceNpWebApiDeleteRequest(request) == 0, "delete the request"); + Require(sceNpWebApiDeleteRequest(request) == RequestNotFound, "request deleted twice"); + Require(sceNpWebApiSendRequest(request, nullptr, 0) == RequestNotFound, "send a deleted request"); + Require(sceNpWebApiDeleteContext(user) == 0, "delete the user context with a request"); + Require(sceNpWebApiSendRequest(second, nullptr, 0) == UserContextNotFound, "request of a deleted user context"); + Require(sceNpWebApiDeleteRequest(second) == UserContextNotFound, "delete a request of a deleted user context"); + Require(sceNpWebApiTerminate(context) == 0, "terminate the request library context"); +} + +void TestPushEventCallbacks() { + const int context = sceNpWebApiInitialize(1, 0x10000); + const int other = sceNpWebApiInitialize(1, 0x10000); + const int user = sceNpWebApiCreateContext(context, onlineId); + Require(user > 0, "user context for callbacks"); + const int filter = sceNpWebApiCreatePushEventFilter(context, filterParam, 1); + const int otherFilter = sceNpWebApiCreatePushEventFilter(other, filterParam, 1); + Require(filter > 0 && otherFilter > 0, "push event filters for callbacks"); + int argument = 0; + + Require(sceNpWebApiRegisterPushEventCallback(user, filter, nullptr, &argument) == InvalidArgument, "push event callback without a function"); + Require(sceNpWebApiRegisterPushEventCallback(user + 1, filter, nullptr, &argument) == InvalidArgument, "the function is checked before the user context"); + Require(sceNpWebApiRegisterPushEventCallback(user + 1, filter, PushEventCallback(), &argument) == UserContextNotFound, "push event callback of an unknown user context"); + Require(sceNpWebApiRegisterPushEventCallback(user, otherFilter, PushEventCallback(), &argument) == PushEventFilterNotFound, "push event callback with another context's filter"); + const int callback = sceNpWebApiRegisterPushEventCallback(user, filter, PushEventCallback(), &argument); + Require(callback > 0 && static_cast(callback) < 0xf0000000u, "push event callback id"); + Require(sceNpWebApiUnregisterPushEventCallback(user + 1, callback) == UserContextNotFound, "unregister through an unknown user context"); + Require(sceNpWebApiUnregisterServicePushEventCallback(user, callback) == ServicePushEventCallbackNotFound, "a push event callback is not a service callback"); + Require(sceNpWebApiUnregisterPushEventCallback(user, callback) == 0, "unregister the push event callback"); + Require(sceNpWebApiUnregisterPushEventCallback(user, callback) == PushEventCallbackNotFound, "push event callback unregistered twice"); + + Require(sceNpWebApiRegisterServicePushEventCallback(user, 1, nullptr, &argument) == InvalidArgument, "service callback without a function"); + Require(sceNpWebApiRegisterServicePushEventCallback(user + 1, 1, PushEventCallback(), &argument) == UserContextNotFound, "service callback of an unknown user context"); + Require(sceNpWebApiRegisterServicePushEventCallback(user, filter, PushEventCallback(), &argument) == ServicePushEventFilterNotFound, "a push event filter is not a service filter"); + Require(sceNpWebApiUnregisterServicePushEventCallback(user, 1) == ServicePushEventCallbackNotFound, "no service callback exists"); + + Require(sceNpWebApiTerminate(context) == 0 && sceNpWebApiTerminate(other) == 0, "terminate the callback library contexts"); +} + +void TestOlderSdks() { + const int context = sceNpWebApiInitialize(1, 0x10000); + const int user = sceNpWebApiCreateContext(context, onlineId); + Require(user > 0, "user context for older SDKs"); + int argument = 0; + + SetSdkVersion(0x02000000); + std::int64_t request = 0; + Require(sceNpWebApiCreateRequest(user, "group", "/path", MethodPatch, nullptr, &request) == 0, "PATCH before SDK 2.50"); + Require(sceNpWebApiSendRequest(request, nullptr, 0) == NotSignedIn, "send a PATCH request"); + Require(sceNpWebApiRegisterPushEventCallback(user, 1, nullptr, &argument) == InvalidArgument, "push event callback without a function from SDK 1.00"); + const int callback = sceNpWebApiRegisterPushEventCallback(user, 1, PushEventCallback(), &argument); + Require(callback > 0, "push event callback without a filter before SDK 2.50"); + Require(sceNpWebApiUnregisterPushEventCallback(user, callback) == 0, "unregister the unfiltered push event callback"); + const int service = sceNpWebApiRegisterServicePushEventCallback(user, 1, PushEventCallback(), &argument); + Require(service > 0 && service != callback, "service callback without a filter before SDK 2.50"); + Require(sceNpWebApiUnregisterPushEventCallback(user, service) == PushEventCallbackNotFound, "a service callback is not a push event callback"); + Require(sceNpWebApiUnregisterServicePushEventCallback(user, service) == 0, "unregister the service callback"); + Require(sceNpWebApiUnregisterServicePushEventCallback(user, service) == ServicePushEventCallbackNotFound, "service callback unregistered twice"); + + SetSdkVersion(0x01000000); + const int handle = sceNpWebApiCreateHandle(context); + Require(sceNpWebApiCreateServicePushEventFilter(context, handle, "service", 0xffffffffu, filterParam, 1) == SignedInUserNotFound, "the invalid service label is accepted before SDK 2.00"); + + SetSdkVersion(0x00900000); + const int unchecked = sceNpWebApiRegisterPushEventCallback(user, 1, nullptr, &argument); + Require(unchecked > 0, "push event callback without a function before SDK 1.00"); + Require(sceNpWebApiUnregisterPushEventCallback(user, unchecked) == 0, "unregister the push event callback without a function"); + Require(sceNpWebApiRegisterServicePushEventCallback(user + 1, 1, nullptr, &argument) == UserContextNotFound, "the user context is checked before the service callback function before SDK 1.00"); + Require(sceNpWebApiRegisterServicePushEventCallback(user, 1, nullptr, &argument) == InvalidArgument, "service callback without a function before SDK 1.00"); + + SetSdkVersion(0x04508001); + Require(sceNpWebApiTerminate(context) == 0, "terminate the older SDK library context"); +} + +} + +int main() { + InstallProcessParameters(); + SetSdkVersion(0x04508001); + TestLibraryContexts(); + TestUserContexts(); + TestRequests(); + TestPushEventCallbacks(); + TestOlderSdks(); return 0; } diff --git a/core/libs/tests/GuestNpWebApi2.cpp b/core/libs/tests/GuestNpWebApi2.cpp index 6144dccf18..291dfdadd8 100644 --- a/core/libs/tests/GuestNpWebApi2.cpp +++ b/core/libs/tests/GuestNpWebApi2.cpp @@ -3,6 +3,9 @@ #include #include #include +#ifdef __APPLE__ +#include +#endif #include #include diff --git a/core/libs/tests/GuestOpusDecVariants.cpp b/core/libs/tests/GuestOpusDecVariants.cpp new file mode 100644 index 0000000000..497d3a1305 --- /dev/null +++ b/core/libs/tests/GuestOpusDecVariants.cpp @@ -0,0 +1,142 @@ +#include "prx/libSceOpusCeltDec/libSceOpusCeltDec.hpp" +#include "prx/libSceOpusDec/libSceOpusDec.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +template +void RequireThrows(TFunction function) { + try { function(); } catch (const std::runtime_error&) { return; } + std::abort(); +} + +const std::uint8_t CELT_PACKET[] = { + 0xFC, 0xB5, 0x1F, 0xBC, 0xC8, 0xCA, 0x0B, 0xC2, 0x18, 0x1A, 0xFB, 0xDB, 0x3E, 0x2B, 0x6B, 0x0A, 0x67, 0x71, 0x25, 0x2F, 0x51, 0x57, 0x4F, 0x78, + 0xB3, 0x1F, 0xE9, 0xF3, 0x99, 0x51, 0xE9, 0x70, 0x72, 0x23, 0x99, 0x3E, 0x26, 0xA3, 0xF1, 0x8F, 0x02, 0xB3, 0xAA, 0xBA, 0xAB, 0x7E, 0xA5, 0x6A, + 0xBA, 0x33, 0xCA, 0xE9, 0x43, 0x4D, 0x41, 0xC9, 0x90, 0x3C, 0xD0, 0x5F, 0x55, 0xC1, 0x54, 0xA8, 0xD0, 0x6F, 0x71, 0x4F, 0xCB, 0x95, 0xDF, 0x09, + 0xC9, 0xB6, 0xAA, 0xA1, 0x8D, 0xD4, 0x2B, 0xC0, 0xF7, 0x23, 0xDD, 0xF5, 0x54, 0xEF, 0x90, 0xC4, 0xD3, 0x47, 0xC1, 0x91, 0x2D, 0xA8, 0x3D, 0x53, + 0xF4, 0x7D, 0xFF, 0xBD, 0x47, 0x0E, 0xD2, 0x95, 0x25, 0x42, 0x66, 0x02, 0xD7, 0x80, 0x00, 0x67, 0x59, 0x13, 0x60, 0x2F, 0x6C, +}; + +struct Api { + std::uint32_t errorBase; + int (APS5_VABI* initialize)(std::uint32_t*); + int (APS5_VABI* terminate)(const std::uint32_t*); + int (APS5_VABI* createEx)(const std::uint32_t*, void*, int, int); + int (APS5_VABI* decode)(void*, const std::uint8_t*, int, std::int16_t*, int); + int (APS5_VABI* decodeWithPriority)(void*, const std::uint8_t*, int, std::int16_t*, int, int); + int (APS5_VABI* decodeFloat)(void*, const std::uint8_t*, int, float*, int); + int (APS5_VABI* decodeFloatWithPriority)(void*, const std::uint8_t*, int, float*, int, int); + int (APS5_VABI* destroy)(void*); +}; + +float Converted(std::int16_t sample) { return sample > 0 ? sample / 32767.0f : sample / 32768.0f; } + +void RequireValidation(const Api& api) { + std::array invalidState{}; + std::int16_t output = 12345; + float floatOutput = 2.0f; + const std::uint8_t packet = 0; + const auto error = [&](std::uint32_t offset) { return static_cast(api.errorBase + offset); }; + Require(api.decodeWithPriority(nullptr, nullptr, -1, nullptr, -1, 0) == error(0)); + Require(api.decodeWithPriority(invalidState.data(), nullptr, -1, nullptr, -1, 0) == error(6)); + Require(api.decodeWithPriority(invalidState.data(), &packet, -1, nullptr, -1, 0) == error(7)); + Require(api.decodeWithPriority(invalidState.data(), &packet, 1, nullptr, -1, 0) == error(8)); + Require(api.decodeWithPriority(invalidState.data(), &packet, 1, &output, -1, 0) == error(9)); + for (int priority : {-1, 0, 19, 101, 255}) + Require(api.decodeWithPriority(invalidState.data(), &packet, 1, &output, 2, priority) == error(10)); + for (int priority : {20, 40, 100}) + Require(api.decodeWithPriority(invalidState.data(), &packet, 1, &output, 2, priority) == error(1)); + Require(api.decodeFloatWithPriority(nullptr, nullptr, -1, nullptr, -1, 0) == error(0)); + Require(api.decodeFloatWithPriority(invalidState.data(), &packet, 1, &floatOutput, -1, 0) == error(9)); + Require(api.decodeFloatWithPriority(invalidState.data(), &packet, 1, &floatOutput, 4, 19) == error(10)); + Require(api.decodeFloatWithPriority(invalidState.data(), &packet, 1, &floatOutput, 4, 100) == error(1)); + Require(api.decodeFloat(invalidState.data(), &packet, 1, nullptr, 4) == error(8)); + Require(api.decodeFloat(invalidState.data(), &packet, 1, &floatOutput, 4) == error(1)); + Require(output == 12345 && floatOutput == 2.0f); +} + +void RequireDecoding(const Api& api) { + std::uint32_t context = 0; + Require(api.initialize(&context) == 0); + std::array, 4> states{}; + for (auto& state : states) Require(api.createEx(&context, state.data(), 48000, 2) == 0); + std::array reference{}, prioritized{}; + std::array floats{}, prioritizedFloats{}; + for (int frame = 0; frame < 3; ++frame) { + reference.fill(12345); + prioritized.fill(12345); + floats.fill(2.0f); + prioritizedFloats.fill(2.0f); + Require(api.decode(states[0].data(), CELT_PACKET, sizeof(CELT_PACKET), reference.data() + 1, 3840) == 3840); + Require(api.decodeWithPriority(states[1].data(), CELT_PACKET, sizeof(CELT_PACKET), prioritized.data() + 1, 3841, 100) == 3840); + Require(api.decodeFloat(states[2].data(), CELT_PACKET, sizeof(CELT_PACKET), floats.data() + 1, 7683) == 7680); + Require(api.decodeFloatWithPriority(states[3].data(), CELT_PACKET, sizeof(CELT_PACKET), prioritizedFloats.data() + 1, 7680, 20) == 7680); + Require(prioritized == reference); + Require(std::any_of(reference.begin() + 1, reference.end() - 1, [](auto value) { return value != 0; })); + Require(floats.front() == 2.0f && floats.back() == 2.0f && floats == prioritizedFloats); + for (std::size_t index = 1; index <= 1920; ++index) Require(floats[index] == Converted(reference[index])); + } + floats.fill(2.0f); + RequireThrows([&] { api.decodeFloat(states[2].data(), CELT_PACKET, sizeof(CELT_PACKET), floats.data() + 1, 7679); }); + Require(std::all_of(floats.begin(), floats.end(), [](auto value) { return value == 2.0f; })); + for (auto& state : states) Require(api.destroy(state.data()) == 0); + Require(api.terminate(&context) == 0); +} + +void RequireTimeoutVariants() { + std::uint32_t context = 0; + Require(sceOpusDecInitialize(&context) == 0); + std::array, 2> states{}; + for (auto& state : states) Require(sceOpusDecCreateEx(&context, state.data(), 48000, 2) == 0); + std::array invalidState{}; + std::int16_t output = 12345; + const std::uint8_t packet = 0; + std::uint32_t batch = 0xA5A5A5A5u; + Require(sceOpusDecDecodeWithPriorityAndTimeout(invalidState.data(), &packet, 1, &output, 2, 19, 0, &batch) == static_cast(0x8174000Au)); + Require(sceOpusDecDecodeWithPriorityAndTimeout(invalidState.data(), &packet, 1, &output, 2, 20, 0xFFFFFFFFu, &batch) == static_cast(0x81740001u)); + Require(sceOpusDecDecodeFloatWithPriorityAndTimeout(nullptr, &packet, 1, nullptr, 2, 20, 0, &batch) == static_cast(0x81740000u)); + Require(sceOpusDecDecodeFloatWithPriorityAndTimeout(invalidState.data(), &packet, -1, nullptr, 2, 20, 1000, &batch) == static_cast(0x81740007u)); + std::array reference{}, pcm{}; + std::array floats{}; + for (int frame = 0; frame < 2; ++frame) { + Require(sceOpusDecDecode(states[0].data(), CELT_PACKET, sizeof(CELT_PACKET), reference.data(), 3840) == 3840); + Require(sceOpusDecDecodeWithPriorityAndTimeout(states[1].data(), CELT_PACKET, sizeof(CELT_PACKET), pcm.data(), 3840, 40, 0xFFFFFFFFu, &batch) == 3840); + Require(pcm == reference); + } + pcm.fill(12345); + floats.fill(2.0f); + for (std::uint32_t timeout : {0u, 1u, 1000u, 0xFFFFFFFEu}) { + RequireThrows([&] { sceOpusDecDecodeWithPriorityAndTimeout(states[1].data(), CELT_PACKET, sizeof(CELT_PACKET), pcm.data(), 3840, 40, timeout, &batch); }); + RequireThrows([&] { sceOpusDecDecodeFloatWithPriorityAndTimeout(states[1].data(), CELT_PACKET, sizeof(CELT_PACKET), floats.data(), 7680, 40, timeout, &batch); }); + } + Require(batch == 0xA5A5A5A5u); + Require(std::all_of(pcm.begin(), pcm.end(), [](auto value) { return value == 12345; })); + Require(std::all_of(floats.begin(), floats.end(), [](auto value) { return value == 2.0f; })); + Require(sceOpusDecDecode(states[0].data(), CELT_PACKET, sizeof(CELT_PACKET), reference.data(), 3840) == 3840); + Require(sceOpusDecDecodeFloatWithPriorityAndTimeout(states[1].data(), CELT_PACKET, sizeof(CELT_PACKET), floats.data(), 7680, 40, 0xFFFFFFFFu, nullptr) == 7680); + for (std::size_t index = 0; index < floats.size(); ++index) Require(floats[index] == Converted(reference[index])); + for (auto& state : states) Require(sceOpusDecDestroy(state.data()) == 0); + Require(sceOpusDecTerminate(&context) == 0); +} + +} // namespace + +int main() { + const Api opus{0x81740000u, sceOpusDecInitialize, sceOpusDecTerminate, sceOpusDecCreateEx, sceOpusDecDecode, + sceOpusDecDecodeWithPriority, sceOpusDecDecodeFloat, sceOpusDecDecodeFloatWithPriority, sceOpusDecDestroy}; + const Api celt{0x81400000u, sceOpusCeltDecInitialize, sceOpusCeltDecTerminate, sceOpusCeltDecCreateEx, sceOpusCeltDecDecode, + sceOpusCeltDecDecodeWithPriority, sceOpusCeltDecDecodeFloat, sceOpusCeltDecDecodeFloatWithPriority, sceOpusCeltDecDestroy}; + for (const auto* api : {&opus, &celt}) { + RequireValidation(*api); + RequireDecoding(*api); + } + RequireTimeoutVariants(); +} diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index e7c2f60371..7fbee7be4d 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -260,6 +260,7 @@ static void CheckLatchedPressLifetime(int handle) { std::this_thread::sleep_for(std::chrono::milliseconds(150)); Require(scePadReadState(handle, &data) == PAD_OK); Require((data.buttons & cross) == 0); + Require(scePadReadState(handle, &data) == PAD_OK); pressed.buttons = cross; PadPublishInput_nid_postfix(pressed); PadPublishInput_nid_postfix(PadInputState{}); @@ -272,6 +273,41 @@ static void CheckLatchedPressLifetime(int handle) { Require((data.buttons & circle) != 0); } +static void CheckLatchedPressAtSlowReads(int handle) { + const auto cross = static_cast(Pad::PadButton::Cross); + PadInputState pressed; + pressed.buttons = cross; + PadData data{}; + Require(scePadReadState(handle, &data) == PAD_OK); + std::this_thread::sleep_for(std::chrono::milliseconds(170)); + Require(scePadReadState(handle, &data) == PAD_OK); + PadPublishInput_nid_postfix(pressed); + PadPublishInput_nid_postfix(PadInputState{}); + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + Require(scePadReadState(handle, &data) == PAD_OK); + Require((data.buttons & cross) != 0); + PadPublishInput_nid_postfix(pressed); + PadPublishInput_nid_postfix(PadInputState{}); + std::this_thread::sleep_for(std::chrono::milliseconds(400)); + Require(scePadReadState(handle, &data) == PAD_OK); + Require((data.buttons & cross) == 0); +} + +static void CheckLatchedPressPerPort() { + const auto cross = static_cast(Pad::PadButton::Cross); + PadInputState pressed; + pressed.buttons = cross; + Pad::ReadState(0); + Pad::ReadState(1); + std::this_thread::sleep_for(std::chrono::milliseconds(170)); + Pad::ReadState(0); + Pad::ReadState(1); + PadPublishInputForPort_nid_postfix(0, pressed); + PadPublishInputForPort_nid_postfix(0, PadInputState{}); + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + Require((Pad::ReadState(0).buttons & cross) != 0); +} + static void CheckReadStateHandle(int handle) { PadData data{}; Require(scePadReadState(0, &data) == PAD_ERROR_INVALID_HANDLE); @@ -322,6 +358,9 @@ static void CheckSpecialPort(int user, int handle) { Require(extended.deviceClass == -1 && extended.reserved[0] == 0); Require(scePadDeviceClassGetExtendedInformation(handle, &extended) == PAD_OK); Require(extended.deviceClass == 0); + std::int32_t specialType = -1; + Require(scePadGetControllerType(special, &specialType) == PAD_OK); + Require(specialType == PAD_CONTROLLER_TYPE_UNKNOWN); const PadVibrationParam vibration{0, 0}; const PadLightBarParam lightBar{0, 0, 255}; Require(scePadSetVibration(special, nullptr) == PAD_ERROR_INVALID_ARG); @@ -589,7 +628,11 @@ int main() { Require(scePadGetHandle(user, 0, 0) == noHandle); Require(scePadOpen_nid_postfix(user, 1, 0, nullptr) == PAD_ERROR_INVALID_ARG); - Require(scePadOpen_nid_postfix(user, 0, 1, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadOpen_nid_postfix(user, 0, 1, nullptr) == static_cast(0x809b0003)); + 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); + for (const int type : {5, 6, 16, 0xff, -1}) Require(scePadOpen_nid_postfix(user, type, 0, nullptr) == PAD_ERROR_INVALID_ARG); Require(scePadGetHandle(user, 0, 0) == noHandle); const unsigned char wheel[16]{0xb7, 0x0e, 0x04, 0x0e, 0x08, 0x6e, 0x01}; Require(scePadOpenExt(user, PAD_PORT_TYPE_SPECIAL, 0, wheel) == static_cast(0x80920007)); @@ -626,6 +669,8 @@ int main() { CheckReadQueue(handle); CheckTapBetweenReads(); CheckLatchedPressLifetime(handle); + CheckLatchedPressAtSlowReads(handle); + CheckLatchedPressPerPort(); CheckReadStateHandle(handle); CheckRemoteController(handle); CheckTrackerDevices(handle); @@ -684,6 +729,9 @@ int main() { std::uint32_t p2AccessState = 7; Require(scePadGetAccessibilityControllerState(handle2, &p2AccessState) == PAD_OK); Require(p2AccessState == 0); + std::int32_t p2Type = -1; + Require(scePadGetControllerType(handle2, &p2Type) == PAD_OK); + Require(p2Type == PAD_CONTROLLER_TYPE_DUAL_SENSE); PadSetPortConnected_nid_postfix(1, true); Require(scePadGetInfo_nid_postfix(&info) == PAD_OK); diff --git a/core/libs/tests/GuestPadVibrationMode.cpp b/core/libs/tests/GuestPadVibrationMode.cpp index 3b82071a1f..4b54861ff0 100644 --- a/core/libs/tests/GuestPadVibrationMode.cpp +++ b/core/libs/tests/GuestPadVibrationMode.cpp @@ -22,5 +22,10 @@ int main() { Require(handle > 0); Require(scePadSetVibrationMode(handle, 0) == PAD_ERROR_INVALID_ARG); Require(scePadSetVibrationMode(handle, 1) == PAD_OK); + Require(scePadSetVibrationMode(handle, 2) == PAD_OK); + Require(scePadSetVibrationMode(handle, 3) == PAD_OK); + Require(scePadSetVibrationMode(handle, 4) == PAD_ERROR_INVALID_ARG); + Require(scePadSetVibrationMode(handle, 5) == PAD_OK); + Require(scePadSetVibrationMode(handle, 6) == PAD_ERROR_INVALID_ARG); Require(scePadClose_nid_postfix(handle) == PAD_OK); } diff --git a/core/libs/tests/GuestPlayGo.cpp b/core/libs/tests/GuestPlayGo.cpp index cf8538a5b3..529e3fd923 100644 --- a/core/libs/tests/GuestPlayGo.cpp +++ b/core/libs/tests/GuestPlayGo.cpp @@ -60,11 +60,15 @@ static int CheckScenarioWithoutChunkTable(const char* root) { int handle = 0; Require(scePlayGoInitialize(&init) == 0); Require(scePlayGoOpen(&handle, nullptr) == 0); - const std::uint16_t anyIds[3] = {1, 10, 0xFFFF}; + const std::uint16_t listedIds[3] = {1, 10, 999}; std::int8_t loci[3] = {0, 0, 0}; - Require(scePlayGoGetLocus(handle, anyIds, 3, loci) == 0 && loci[0] == 3 && loci[1] == 3 && loci[2] == 3); - const PlayGoToDo todo[2] = {{0, 3, 0}, {0xFFFF, 3, 0}}; + Require(scePlayGoGetLocus(handle, listedIds, 3, loci) == 0 && loci[0] == 3 && loci[1] == 3 && loci[2] == 3); + const PlayGoToDo todo[2] = {{0, 3, 0}, {999, 3, 0}}; Require(scePlayGoSetToDoList(handle, todo, 2) == 0); + for (const std::uint16_t unlisted : {std::uint16_t{1000}, std::uint16_t{0xFFFF}}) { + std::int8_t locus = 0; + RequireMissingChunkMetadata([&] { scePlayGoGetLocus(handle, &unlisted, 1, &locus); }); + } std::uint32_t chunkCount = 0; Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 1000); std::vector ids(chunkCount + 1, 0xFFFF); @@ -197,6 +201,27 @@ static int CheckNoPlayGoFileAnswered(const char* root, const char* answer) { return 0; } +static int CheckScenarioWithoutChunkTableAnswered(const char* root, const char* answer) { + constexpr int badChunkId = static_cast(0x80B2000C); + SetUnlistedChunks(answer); + std::filesystem::current_path(root); + PlayGoInitParams init{}; + int handle = 0; + Require(scePlayGoInitialize(&init) == 0); + Require(scePlayGoOpen(&handle, nullptr) == 0); + const std::uint16_t unlisted[2] = {1000, 0xFFFF}; + std::int8_t loci[2] = {0, 0}; + const int result = scePlayGoGetLocus(handle, unlisted, 2, loci); + if (std::string_view(answer) == "installed") Require(result == 0 && loci[0] == 3 && loci[1] == 3); + else Require(result == badChunkId); + const std::uint16_t listed[2] = {0, 999}; + 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); + Require(scePlayGoClose(handle) == 0); + return 0; +} + static int CheckUnknownUnlistedChunkAnswer(const char* root) { SetUnlistedChunks("maybe"); std::filesystem::current_path(root); @@ -249,6 +274,8 @@ int main(int argc, char** argv) { if (mode == "--no-file-unknown-answer") return CheckUnknownUnlistedChunkAnswer(argv[2]); if (mode == "--package-chunk-table" || mode == "--scenario-with-chunk-table") return CheckPackageChunkTable(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"); if (mode == "--ficm-only") return CheckRejectedLayout(argv[2], "playgo-ficm.dat"); if (mode == "--hash-table-only") return CheckRejectedLayout(argv[2], "playgo-hash-table.dat"); if (mode == "--invalid-chunk-table") return CheckRejectedLayout(argv[2], "playgo-chunk.dat"); @@ -260,7 +287,25 @@ int main(int argc, char** argv) { RunLayout(argv[0], "--package-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}}); RunLayout(argv[0], "--package-chunk-table", {{"playgo-chunk.dat", ChunkTableFile("plgo")}}); RunLayout(argv[0], "--scenario-with-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}, {"playgo-scenario.json", "{}"}}); + std::int64_t eta = -1; + std::int32_t speed = -1; + PlayGoInitParams fixtureInit{}; + int validHandle = -1; + Require(scePlayGoInitialize(&fixtureInit) == 0); + Require(scePlayGoOpen(&validHandle, nullptr) == 0); + constexpr int badHandleError = static_cast(0x80B20009); + constexpr int badPointerError = static_cast(0x80B2000A); + Require(scePlayGoGetEta(-1, nullptr, 0, &eta) == badHandleError); + Require(scePlayGoGetEta(validHandle, nullptr, 0, nullptr) == badPointerError); + Require(scePlayGoGetInstallSpeed(-1, &speed) == badHandleError); + Require(scePlayGoGetInstallSpeed(0x506C476F, &speed) == badHandleError && speed == -1); + Require(scePlayGoGetInstallSpeed(validHandle, nullptr) == badPointerError); + Require(scePlayGoGetInstallSpeed(validHandle, &speed) == 0 && speed == 2); + Require(scePlayGoPrefetch(-1, nullptr, 0, 0) == badHandleError); + Require(scePlayGoClose(validHandle) == 0); RunLayout(argv[0], "--scenario-only", {{"playgo-scenario.json", "{}"}}); + RunLayout(argv[0], "--scenario-only-installed", {{"playgo-scenario.json", "{}"}}); + RunLayout(argv[0], "--scenario-only-absent", {{"playgo-scenario.json", "{}"}}); RunLayout(argv[0], "--ficm-only", {{"playgo-ficm.dat", "x"}}); RunLayout(argv[0], "--hash-table-only", {{"playgo-hash-table.dat", "x"}}); RunLayout(argv[0], "--invalid-chunk-table", {{"playgo-chunk.dat", "not a chunk table"}}); diff --git a/core/libs/tests/GuestPngDec.cpp b/core/libs/tests/GuestPngDec.cpp index f7dab4a743..120be43fdc 100644 --- a/core/libs/tests/GuestPngDec.cpp +++ b/core/libs/tests/GuestPngDec.cpp @@ -4,21 +4,18 @@ #include "Decoder/Png.hpp" #include #include +#include #include #include -#include +#include +#include +#include #include -static void Require(bool value) { if (!value) std::abort(); } - -template -static bool ThrowsRuntimeError(TFunction function) { - try { - function(); - } catch (const std::runtime_error&) { - return true; - } - return false; +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "%s:%u: PNG decoder assertion failed\n", location.file_name(), location.line()); + std::abort(); } static const std::uint8_t PALETTE_TRNS[] = { @@ -39,6 +36,44 @@ static const std::uint8_t GRAY16[] = { 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, }; +static const std::uint8_t GRAY16_TRNS[] = { + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x10, 0x00, 0x00, 0x00, 0x00, 0x07, 0x4D, 0x8E, + 0xBB, 0x00, 0x00, 0x00, 0x02, 0x74, 0x52, 0x4E, 0x53, 0x12, 0x34, 0x2F, 0xD3, 0x49, 0x5E, 0x00, + 0x00, 0x00, 0x12, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x10, 0x32, 0x11, 0x5A, 0xCD, 0xB0, + 0xFA, 0x2C, 0x03, 0x03, 0x00, 0x0B, 0xDF, 0x02, 0x7C, 0x4E, 0x3C, 0x25, 0x55, 0x00, 0x00, 0x00, + 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, +}; + +static const std::uint8_t RGB16_TRNS[] = { + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x10, 0x02, 0x00, 0x00, 0x00, 0xAD, 0x44, 0x46, + 0x30, 0x00, 0x00, 0x00, 0x06, 0x74, 0x52, 0x4E, 0x53, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0x89, + 0xE4, 0x4E, 0xE6, 0x00, 0x00, 0x00, 0x23, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x10, 0x32, + 0x09, 0xAB, 0x98, 0xB5, 0x47, 0xE8, 0x7F, 0xD8, 0xFF, 0x59, 0xFF, 0x19, 0xFE, 0x33, 0x30, 0x30, + 0x36, 0x30, 0x32, 0x30, 0x34, 0x30, 0xFC, 0xFF, 0x0F, 0x00, 0x88, 0xFB, 0x0A, 0x69, 0xC3, 0x3F, + 0x47, 0x19, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, +}; + +static const std::uint8_t GRAY_ALPHA16[] = { + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x10, 0x04, 0x00, 0x00, 0x00, 0x88, 0x2F, 0x19, + 0xEC, 0x00, 0x00, 0x00, 0x19, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x10, 0x32, 0xB9, 0xF7, + 0x61, 0xF5, 0x59, 0x06, 0x20, 0xF8, 0xF7, 0xFF, 0xFF, 0x7F, 0xC6, 0x86, 0x46, 0x00, 0x48, 0x58, + 0x08, 0x8A, 0x8F, 0xC8, 0x5C, 0x92, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, + 0x60, 0x82, +}; + +static const std::uint8_t RGBA16[] = { + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x10, 0x06, 0x00, 0x00, 0x00, 0x22, 0x26, 0xD1, + 0x67, 0x00, 0x00, 0x00, 0x2D, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x01, 0x22, 0x00, 0xDD, 0xFF, + 0x00, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0x0D, 0xEF, 0xAB, 0xCD, 0x20, 0x01, 0xEE, 0xFE, 0xFF, + 0xFF, 0x00, 0x00, 0x01, 0x80, 0x40, 0xFF, 0xAB, 0x00, 0x00, 0xFF, 0xFF, 0x01, 0x02, 0x7F, 0xFE, + 0x80, 0xFF, 0x07, 0xD0, 0x10, 0x52, 0x90, 0xA2, 0xAC, 0x25, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, + 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, +}; + static std::vector RgbPng() { std::vector rgb(3 * 2 * 3); for (std::size_t i = 0; i < rgb.size(); ++i) rgb[i] = static_cast(10 + i * 13); @@ -63,6 +98,121 @@ static PngDecDecodeParam DecodeParam(const std::vector& png, std:: return param; } +static void Check16BitImage(void* limitedHandle, void* enabledHandle, std::span png, + std::initializer_list expected, std::uint16_t colorSpace, + std::uint32_t flags, std::uint32_t height = 2) { + constexpr std::int32_t invalidSize = static_cast(0x80690002); + constexpr std::int32_t invalidParam = static_cast(0x80690003); + constexpr std::int32_t invalidWorkMemory = static_cast(0x80690005); + const std::vector input(png.begin(), png.end()); + const std::vector expectedPixels(expected); + Require(expectedPixels.size() == 2 * height * 4); + for (const std::uint16_t format : {0, 1}) { + for (const std::uint32_t padding : {0, 8}) { + const std::uint32_t pitch = 8 + padding; + const std::uint32_t size = pitch * height; + std::vector output(size + 16, 0xCC); + PngDecDecodeParam decode = DecodeParam(input, output); + decode.pixel_format = format; + decode.image_pitch = padding == 0 ? 0 : pitch; + decode.image_mem_size = size; + PngDecImageInfo info{}; + Require(scePngDecDecode(limitedHandle, &decode, &info) == invalidWorkMemory); + Require(info.image_width == 2 && info.image_height == height && info.bit_depth == 16); + Require(info.color_space == colorSpace && info.image_flag == flags); + Require(std::all_of(output.begin(), output.end(), [](std::uint8_t byte) { return byte == 0xCC; })); + Require(scePngDecDecode(enabledHandle, &decode, format == 0 ? &info : nullptr) == static_cast(2 << 16 | height)); + Require(info.image_width == 2 && info.image_height == height && info.bit_depth == 16); + Require(info.color_space == colorSpace && info.image_flag == flags); + for (std::uint32_t y = 0; y < height; ++y) { + for (std::uint32_t x = 0; x < 2; ++x) { + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const std::uint32_t inputChannel = format == 1 && channel < 3 ? 2 - channel : channel; + Require(output[y * pitch + x * 4 + channel] == expectedPixels[y * 8 + x * 4 + inputChannel]); + } + } + for (std::uint32_t offset = 8; offset < pitch; ++offset) Require(output[y * pitch + offset] == 0xCC); + } + Require(std::all_of(output.begin() + size, output.end(), [](std::uint8_t byte) { return byte == 0xCC; })); + std::fill(output.begin(), output.end(), 0xCC); + decode.image_mem_size = size - 1; + for (void* handle : {limitedHandle, enabledHandle}) { + Require(scePngDecDecode(handle, &decode, &info) == (padding == 0 ? invalidSize : invalidParam)); + Require(std::all_of(output.begin(), output.end(), [](std::uint8_t byte) { return byte == 0xCC; })); + const auto validPitch = decode.image_pitch; + decode.image_pitch = 7; + Require(scePngDecDecode(handle, &decode, &info) == invalidParam); + Require(std::all_of(output.begin(), output.end(), [](std::uint8_t byte) { return byte == 0xCC; })); + decode.image_pitch = validPitch; + } + } + } +} + +static void CheckDimensionReporting(std::uint32_t width, std::uint32_t height) { + constexpr std::int32_t invalidAddr = static_cast(0x80690001); + constexpr std::int32_t invalidSize = static_cast(0x80690002); + constexpr std::int32_t invalidParam = static_cast(0x80690003); + constexpr std::int32_t invalidData = static_cast(0x80690010); + std::vector rgb(static_cast(width) * height * 3); + for (std::size_t i = 0; i < rgb.size(); ++i) rgb[i] = static_cast(i * 13 + 7); + const auto png = Decoder::Png::Encode(rgb, width, height, 3); + const PngDecCreateParam create{sizeof(PngDecCreateParam), 0, width}; + const auto memorySize = scePngDecQueryMemorySize(&create); + Require(memorySize > 0); + std::vector memory(memorySize); + void* handle = nullptr; + Require(scePngDecCreate(&create, memory.data(), memorySize, &handle) == 0); + const bool packed = width <= 32767 && height <= 32767; + const auto result = packed ? static_cast(width << 16 | height) : 0; + for (const std::uint16_t format : {0, 1}) { + for (const std::uint32_t padding : {0, 8}) { + const std::uint32_t pitch = width * 4 + padding; + const std::uint32_t size = pitch * height; + std::vector image(size + 16, 0xCC); + auto decode = DecodeParam(png, image); + decode.pixel_format = format; + decode.image_pitch = padding == 0 ? 0 : pitch; + decode.image_mem_size = size; + Require(scePngDecDecode(handle, &decode, nullptr) == (packed ? result : invalidAddr)); + if (!packed) Require(std::all_of(image.begin(), image.end(), [](auto byte) { return byte == 0xCC; })); + PngDecImageInfo info{}; + Require(scePngDecDecode(handle, &decode, &info) == result); + Require(info.image_width == width && info.image_height == height && info.color_space == 3 && info.bit_depth == 8 && info.image_flag == 0); + for (std::uint32_t y = 0; y < height; ++y) { + for (std::uint32_t x = 0; x < width; ++x) { + for (std::uint32_t channel = 0; channel < 3; ++channel) { + const auto inputChannel = format == 0 ? channel : 2 - channel; + Require(image[y * pitch + x * 4 + channel] == rgb[(static_cast(y) * width + x) * 3 + inputChannel]); + } + Require(image[y * pitch + x * 4 + 3] == 0x77); + } + for (std::uint32_t offset = width * 4; offset < pitch; ++offset) Require(image[y * pitch + offset] == 0xCC); + } + Require(std::all_of(image.begin() + size, image.end(), [](auto byte) { return byte == 0xCC; })); + if (packed) continue; + std::fill(image.begin(), image.end(), 0xCC); + decode.image_mem_size = 1; + Require(scePngDecDecode(handle, &decode, nullptr) == invalidAddr); + Require(scePngDecDecode(handle, &decode, &info) == (padding == 0 ? invalidSize : invalidParam)); + decode.image_mem_size = size; + decode.image_pitch = 1; + Require(scePngDecDecode(handle, &decode, nullptr) == invalidAddr); + Require(scePngDecDecode(handle, &decode, &info) == invalidParam); + decode.image_mem_size = 0; + Require(scePngDecDecode(handle, &decode, nullptr) == invalidSize); + decode.image_mem_size = size; + decode.pixel_format = 2; + Require(scePngDecDecode(handle, &decode, nullptr) == invalidParam); + decode.pixel_format = format; + decode.png_mem_size = 8; + Require(scePngDecDecode(handle, &decode, nullptr) == invalidData); + Require(std::all_of(image.begin(), image.end(), [](auto byte) { return byte == 0xCC; })); + } + } + Require(scePngDecDelete(handle) == 0); +} + int main() { constexpr std::int32_t invalidAddr = static_cast(0x80690001); constexpr std::int32_t invalidSize = static_cast(0x80690002); @@ -198,10 +348,10 @@ int main() { Require(info.image_flag == 2); const std::vector gray16Png(GRAY16, GRAY16 + sizeof(GRAY16)); - std::vector gray16Image(2 * 4); + std::vector gray16Image(2 * 4, 0xCC); decode = DecodeParam(gray16Png, gray16Image); - Require(scePngDecDecode(handle, &decode, &info) == (2 << 16 | 1)); - Require(gray16Image == std::vector({0x12, 0x12, 0x12, 0x77, 0xAB, 0xAB, 0xAB, 0x77})); + Require(scePngDecDecode(handle, &decode, &info) == invalidWorkMemory); + Require(std::all_of(gray16Image.begin(), gray16Image.end(), [](std::uint8_t byte) { return byte == 0xCC; })); Require(info.bit_depth == 16); decode = DecodeParam(rgbPng, image); @@ -251,8 +401,11 @@ int main() { Require(deepSize == 85136); std::vector deepMemory(static_cast(deepSize)); Require(scePngDecCreate(&deepParam, deepMemory.data(), static_cast(deepSize), &deepHandle) == 0); - decode = DecodeParam(gray16Png, gray16Image); - Require(ThrowsRuntimeError([&] { scePngDecDecode(deepHandle, &decode, &info); })); + Check16BitImage(handle, deepHandle, GRAY16, {0x12, 0x12, 0x12, 0x77, 0xAB, 0xAB, 0xAB, 0x77}, 2, 0, 1); + Check16BitImage(handle, deepHandle, GRAY16_TRNS, {0x12, 0x12, 0x12, 0, 0x12, 0x12, 0x12, 0xFF, 0xAB, 0xAB, 0xAB, 0xFF, 0, 0, 0, 0xFF}, 2, 2); + Check16BitImage(handle, deepHandle, RGB16_TRNS, {0x12, 0x56, 0x9A, 0, 0x12, 0x56, 0x9A, 0xFF, 0xFF, 0, 0x80, 0xFF, 0, 0x80, 0xFF, 0xFF}, 3, 2); + Check16BitImage(handle, deepHandle, GRAY_ALPHA16, {0x12, 0x12, 0x12, 0xDE, 0xAB, 0xAB, 0xAB, 0, 0, 0, 0, 0xFF, 0xFF, 0xFF, 0xFF, 0x80}, 18, 0); + Check16BitImage(handle, deepHandle, RGBA16, {0x12, 0x56, 0x9A, 0x0D, 0xAB, 0x20, 0xEE, 0xFF, 0, 0x80, 0xFF, 0, 0xFF, 1, 0x7F, 0x80}, 19, 0); decode = DecodeParam(rgbPng, image); Require(scePngDecDecode(deepHandle, &decode, &info) == (3 << 16 | 2)); Require(scePngDecDelete(deepHandle) == 0); @@ -263,5 +416,9 @@ int main() { Require(scePngDecDelete(handle) == invalidHandle); Require(scePngDecDecode(handle, &decode, &info) == invalidHandle); + for (const std::uint32_t dimension : {32767, 32768, 65536}) { + CheckDimensionReporting(dimension, 1); + CheckDimensionReporting(1, dimension); + } return 0; } diff --git a/core/libs/tests/GuestPoll.cpp b/core/libs/tests/GuestPoll.cpp index 8d9c7ee484..c5241498ea 100644 --- a/core/libs/tests/GuestPoll.cpp +++ b/core/libs/tests/GuestPoll.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -104,6 +105,26 @@ static void PollsFilesAndDirectories(int socket) { Require(rmdir_nid_postfix(directoryPath.c_str()) == 0 && unlink_nid_postfix(filePath.c_str()) == 0); } +static void StandardDescriptorFile(int descriptor) { + const int saved = dup_nid_postfix(descriptor); + const std::string path = "anyps5-poll-input-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const int file = open_nid_postfix(path.c_str(), 0x0002 | 0x0200, 0644); + Require(file >= 0); + Require(dup2_nid_postfix(file, descriptor) == descriptor); + if (file != descriptor) Require(close_nid_postfix(file) == 0); + GuestPollDescriptor input{descriptor, static_cast(In | Out | ReadNormal | Priority), -1}; + const int ready = poll_nid_postfix(&input, 1, 0); + if (ready != 1) std::fprintf(stderr, "poll on a regular file at descriptor %d returned %d instead of 1\n", descriptor, ready); + Require(ready == 1 && input.revents == (In | Out | ReadNormal)); + input.events = Priority; + Require(poll_nid_postfix(&input, 1, 0) == 0 && input.revents == 0); + if (saved >= 0) { + Require(dup2_nid_postfix(saved, descriptor) == descriptor); + Require(close_nid_postfix(saved) == 0); + } else Require(close_nid_postfix(descriptor) == 0); + Require(unlink_nid_postfix(path.c_str()) == 0); +} + static void TestPipes(int socket) { int ends[2]; Require(pipe_nid_postfix(ends) == 0); @@ -261,9 +282,37 @@ static void PollsAStreamWhosePeerClosed() { for (const int descriptor : {client, server, listener}) Require(close_nid_postfix(descriptor) == 0); } +#ifndef _WIN32 +static void PollsAHungUpSocketWithoutPollout() { + const int listener = socket_nid_postfix(2, 1, 0); + Require(listener >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &size) == 0 && listen_nid_postfix(listener, 1) == 0); + const int client = socket_nid_postfix(2, 1, 0); + Require(client >= 0 && connect_nid_postfix(client, address.data(), address.size()) == 0); + const int server = accept_nid_postfix(listener, nullptr, nullptr); + Require(server >= 0); + Require(close_nid_postfix(client) == 0); + char byte = 0; + GuestPollDescriptor ended{server, In, 0}; + Require(poll_nid_postfix(&ended, 1, 1000) == 1); + Require(recv_nid_postfix(server, &byte, 1, 0) == 0); + Require(shutdown_nid_postfix(server, 1) == 0); + GuestPollDescriptor hungUp{server, static_cast(In | Out), 0}; + Require(poll_nid_postfix(&hungUp, 1, 1000) == 1 && hungUp.revents == (In | Hup)); + Require(close_nid_postfix(server) == 0 && close_nid_postfix(listener) == 0); +} +#endif + int main() { + for (const int descriptor : {0, 1, 2}) StandardDescriptorFile(descriptor); PollsUrgentData(); PollsAStreamWhosePeerClosed(); +#ifndef _WIN32 + PollsAHungUpSocketWithoutPollout(); +#endif const int listener = socket_nid_postfix(2, 1, 0); Require(listener >= 0 && listener < 1024); std::array address{16, 2, 0, 0, 127, 0, 0, 1}; diff --git a/core/libs/tests/GuestPosixSem.cpp b/core/libs/tests/GuestPosixSem.cpp index 89e0a0b511..bb2dc5f570 100644 --- a/core/libs/tests/GuestPosixSem.cpp +++ b/core/libs/tests/GuestPosixSem.cpp @@ -18,6 +18,29 @@ static void Require(bool condition, const char* message) { } } +static void CheckRetire() { + constexpr int rounds = 20000; + std::atomic published{nullptr}; + std::thread poster([&] { + for (int round = 0; round < rounds; ++round) { + published.wait(nullptr, std::memory_order_acquire); + std::uintptr_t* target = published.exchange(nullptr, std::memory_order_acq_rel); + Require(sem_post_nid_postfix(target) == 0, "post to a semaphore its waiter destroys must succeed"); + } + }); + for (int round = 0; round < rounds; ++round) { + std::uintptr_t slot = 0; + Require(sem_init_nid_postfix(&slot, 0, 0) == 0, "retired semaphore must initialize"); + published.store(&slot, std::memory_order_release); + published.notify_one(); + for (int spins = 0; sem_trywait_nid_postfix(&slot) != 0; ++spins) { + if (spins >= 4096) std::this_thread::yield(); + } + Require(sem_destroy_nid_postfix(&slot) == 0, "semaphore must be destroyable once its count is taken"); + } + poster.join(); +} + int main(int argc, char** argv) { constexpr unsigned int maximum = 0x7fffffffu; std::uintptr_t sem = 0; @@ -91,4 +114,5 @@ int main(int argc, char** argv) { Require(sem_timedwait_nid_postfix(&sem, &expiredTimeout) == -1 && *__error_nid_postfix() == 60, "blocked expired-timeout wait must report ETIMEDOUT"); Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "failed timed waits must preserve the count"); Require(sem_destroy_nid_postfix(&sem) == 0, "blocked semaphore destroy must succeed"); + CheckRetire(); } diff --git a/core/libs/tests/GuestPosixSpawn.cpp b/core/libs/tests/GuestPosixSpawn.cpp index ecfb146655..d71e652950 100644 --- a/core/libs/tests/GuestPosixSpawn.cpp +++ b/core/libs/tests/GuestPosixSpawn.cpp @@ -1,8 +1,10 @@ #include "prx/libc/include/Api.hpp" #include "prx/libSceLibcInternal/include/Api.hpp" #include "prx/libSceLibcInternal/include/Spawn.hpp" +#include "prx/libkernel/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include @@ -11,6 +13,17 @@ #include #include #include +#include +#include +#ifdef _WIN32 +#include +#else +extern "C" int kill(int, int); +#endif + +#if defined(__linux__) +extern char** environ; +#endif static void Require(bool condition, int line) { if (!condition) { @@ -150,29 +163,29 @@ static void CheckSpawn() { std::int32_t processId = 77; Check(ReportsNotImplemented([&] { posix_spawn_nid_postfix(&processId, existing.c_str(), &actions, &attributes, arguments, environment); }, - "posix_spawn: process creation not implemented")); + "posix_spawn: executables outside /app0 (" + existing + ") not implemented")); Check(posix_spawn_nid_postfix(&processId, missing.c_str(), &actions, &attributes, arguments, environment) == NoSuchFile); Check(posix_spawn_nid_postfix(&processId, "", nullptr, nullptr, arguments, nullptr) == NoSuchFile); Check(posix_spawn_nid_postfix(&processId, "tool.elf", nullptr, nullptr, arguments, nullptr) == NoSuchFile); Check(setenv_nid_postfix("PATH", bothDirectories.c_str(), 1) == 0); Check(ReportsNotImplemented([&] { posix_spawnp_nid_postfix(&processId, "tool.elf", &actions, &attributes, arguments, nullptr); }, - "posix_spawnp: process creation not implemented")); + "posix_spawnp: executables outside /app0 (" + existing + ") not implemented")); Check(posix_spawnp_nid_postfix(&processId, "missing.elf", &actions, &attributes, arguments, nullptr) == NoSuchFile); Check(posix_spawnp_nid_postfix(&processId, "", nullptr, nullptr, arguments, nullptr) == NoSuchFile); Check(setenv_nid_postfix("PATH", emptyOnly.c_str(), 1) == 0); Check(posix_spawnp_nid_postfix(&processId, "tool.elf", nullptr, nullptr, arguments, nullptr) == NoSuchFile); Check(ReportsNotImplemented([&] { posix_spawnp_nid_postfix(&processId, existing.c_str(), nullptr, nullptr, arguments, nullptr); }, - "posix_spawnp: process creation not implemented")); + "posix_spawnp: executables outside /app0 (" + existing + ") not implemented")); Check(posix_spawnp_nid_postfix(&processId, missing.c_str(), nullptr, nullptr, arguments, nullptr) == NoSuchFile); Check(setenv_nid_postfix("PATH", (emptyOnly + ":").c_str(), 1) == 0); Check(posix_spawnp_nid_postfix(&processId, "tool.elf", nullptr, nullptr, arguments, nullptr) == NoSuchFile); Check(chdir_nid_postfix(guest.c_str()) == 0); Check(ReportsNotImplemented([&] { posix_spawnp_nid_postfix(&processId, "tool.elf", nullptr, nullptr, arguments, nullptr); }, - "posix_spawnp: process creation not implemented")); + "posix_spawnp: executables outside /app0 (./tool.elf) not implemented")); Check(ReportsNotImplemented([&] { posix_spawn_nid_postfix(&processId, "tool.elf", nullptr, nullptr, arguments, nullptr); }, - "posix_spawn: process creation not implemented")); + "posix_spawn: executables outside /app0 (tool.elf) not implemented")); Check(chdir_nid_postfix("/") == 0); Check(unsetenv_nid_postfix("PATH") == 0); @@ -190,8 +203,170 @@ static void CheckSpawn() { std::filesystem::remove_all(directory); } +static std::filesystem::path OwnExecutablePath() { +#ifdef _WIN32 + std::wstring own(32768, L'\0'); + const DWORD length = GetModuleFileNameW(nullptr, own.data(), static_cast(own.size())); + own.resize(length); + return std::filesystem::path(own); +#else + std::error_code error; + return std::filesystem::read_symlink("/proc/self/exe", error); +#endif +} + +static std::int32_t SpawnChild(const char* role) { + const auto own = OwnExecutablePath(); + const auto target = own.parent_path() / std::filesystem::path(reinterpret_cast(("child.bin" + own.extension().string()).c_str())); + std::error_code error; + std::filesystem::copy_file(own, target, std::filesystem::copy_options::overwrite_existing, error); + Check(!error); + const auto app0 = std::filesystem::current_path() / "app0"; + Check(std::filesystem::create_directories(app0) || std::filesystem::exists(app0)); + { std::ofstream file(app0 / "child.bin"); file << "child"; } + + char program[] = "/app0/child.bin"; + char* arguments[] = {program, nullptr}; + std::vector entries; +#ifdef _WIN32 + char** const parentEnvironment = _environ; +#else + char** const parentEnvironment = environ; +#endif + for (char** entry = parentEnvironment; entry != nullptr && *entry != nullptr; ++entry) + entries.push_back(*entry); + entries.push_back(std::string("ANYPS5_POSIX_SPAWN_ROLE=") + role); + std::vector environment; + for (auto& entry : entries) + environment.push_back(entry.data()); + environment.push_back(nullptr); + std::int32_t processId = 0; + Check(posix_spawn_nid_postfix(&processId, program, nullptr, nullptr, arguments, environment.data()) == 0); + Check(processId > 0); + return processId; +} + +static void CleanupChild() { + std::error_code error; + std::filesystem::remove(OwnExecutablePath().parent_path() / std::filesystem::path(reinterpret_cast(("child.bin" + OwnExecutablePath().extension().string()).c_str())), error); + std::filesystem::remove_all(std::filesystem::current_path() / "app0", error); +} + +static void CheckSpawnedChild(const char* role, int expectedCode) { + const std::int32_t processId = SpawnChild(role); + int status = -1; + if (std::strcmp(role, "child-sleep") == 0) + Check(waitpid_nid_postfix(processId, &status, 1) == 0); + Check(waitpid_nid_postfix(processId, &status, 0) == processId); + Check((status & 0x7f) == 0 && ((status >> 8) & 0xff) == expectedCode); + status = -1; + Check(waitpid_nid_postfix(processId, &status, 0) == -1 && *__error_nid_postfix() == 10); + CleanupChild(); +} + +static void CheckConcurrentWaiters() { + const std::int32_t processId = SpawnChild("child-sleep"); + int statusA = -1; + int statusB = -1; + int errorA = 0; + int errorB = 0; + int resultA = 0; + int resultB = 0; + const auto waiter = [processId](int* status, int* error) { + const int result = waitpid_nid_postfix(processId, status, 0); + *error = *__error_nid_postfix(); + return result; + }; + std::thread threadA([&] { resultA = waiter(&statusA, &errorA); }); + std::thread threadB([&] { resultB = waiter(&statusB, &errorB); }); + threadA.join(); + threadB.join(); + const bool reapedA = resultA == processId; + const bool reapedB = resultB == processId; + Check(reapedA != reapedB); + const int reapedStatus = reapedA ? statusA : statusB; + Check((reapedStatus & 0x7f) == 0 && ((reapedStatus >> 8) & 0xff) == 43); + Check((reapedA ? errorB : errorA) == 10); + CleanupChild(); +} + +#ifndef _WIN32 +static void CheckStoppedChild() { + const std::int32_t processId = SpawnChild("child-stop"); + int status = -1; + Check(waitpid_nid_postfix(processId, &status, 2) == processId); + Check((status & 0x7f) == 0x7f && (status >> 8) == 17); + Check(kill(processId, SIGCONT) == 0); + Check(waitpid_nid_postfix(processId, &status, 4) == processId && status == 0x13); + Check(waitpid_nid_postfix(processId, &status, 0) == processId); + Check((status & 0x7f) == 0 && ((status >> 8) & 0xff) == 44); + Check(waitpid_nid_postfix(processId, &status, 0) == -1 && *__error_nid_postfix() == 10); + CleanupChild(); +} + +static void CheckKilledChild(const char* role, int guestSignal) { + const std::int32_t processId = SpawnChild(role); + int status = -1; + Check(waitpid_nid_postfix(processId, &status, 0) == processId); + Check((status & 0x7f) == guestSignal); + Check(waitpid_nid_postfix(processId, &status, 0) == -1 && *__error_nid_postfix() == 10); + CleanupChild(); +} +#else +static void CheckTerminatedChild(const char* role, unsigned code, int guestSignal) { + const std::int32_t processId = SpawnChild(role); + const HANDLE process = OpenProcess(PROCESS_TERMINATE, FALSE, static_cast(processId)); + Check(process != nullptr); + Check(TerminateProcess(process, code) != 0); + CloseHandle(process); + int status = -1; + Check(waitpid_nid_postfix(processId, &status, 0) == processId); + Check(code == 0xFFFFFFFFu ? ((status & 0x7f) == 0 && ((status >> 8) & 0xff) == 255) + : ((status & 0x7f) == guestSignal && (status & 0x80) == 0)); + Check(waitpid_nid_postfix(processId, &status, 0) == -1 && *__error_nid_postfix() == 10); + CleanupChild(); +} +#endif + int main() { + if (const char* role = std::getenv("ANYPS5_POSIX_SPAWN_ROLE")) { + if (std::strcmp(role, "child-exit-42") == 0) std::_Exit(42); + if (std::strcmp(role, "child-exit-neg") == 0) std::exit(-1); +#ifndef _WIN32 + if (std::strcmp(role, "child-stop") == 0) { + std::raise(SIGSTOP); + std::_Exit(44); + } + if (std::strcmp(role, "child-kill-bus") == 0) std::raise(SIGBUS); + if (std::strcmp(role, "child-kill-sys") == 0) std::raise(SIGSYS); +#endif + if (std::strcmp(role, "child-sleep") == 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + std::_Exit(43); + } + if (std::strcmp(role, "child-laze") == 0) { + std::this_thread::sleep_for(std::chrono::seconds(30)); + std::_Exit(47); + } + std::_Exit(99); + } CheckAttributes(); CheckFileActions(); CheckSpawn(); + CheckSpawnedChild("child-exit-42", 42); + CheckSpawnedChild("child-sleep", 43); + CheckSpawnedChild("child-exit-neg", 255); + CheckConcurrentWaiters(); +#ifndef _WIN32 + CheckStoppedChild(); + CheckKilledChild("child-kill-bus", 10); + CheckKilledChild("child-kill-sys", 12); +#else + CheckTerminatedChild("child-laze", 0xC0000005u, 11); + CheckTerminatedChild("child-laze", 0xC0000096u, 4); + CheckTerminatedChild("child-laze", 0x80000003u, 5); + CheckTerminatedChild("child-laze", 0xC0000094u, 8); + CheckTerminatedChild("child-laze", 0xC0000409u, 6); + CheckTerminatedChild("child-laze", 0xFFFFFFFFu, 0); +#endif } diff --git a/core/libs/tests/GuestPosixThreadName.cpp b/core/libs/tests/GuestPosixThreadName.cpp new file mode 100644 index 0000000000..05f284f0db --- /dev/null +++ b/core/libs/tests/GuestPosixThreadName.cpp @@ -0,0 +1,46 @@ +#include "SceTypes.hpp" +#include "prx/libScePosixForWebKit/include/Api.hpp" +#include "prx/libkernel/include/Api.hpp" +#include +#include +#include +#include + +static constexpr int GUEST_EINVAL = 22; +static constexpr int GUEST_ENAMETOOLONG = 63; + +static void Require(bool value) { if (!value) std::abort(); } + +static std::string ReadName(Pthread thread) { + std::array buffer{}; + buffer.fill(0x55); + Require(pthread_getname_np_nid_postfix(thread, buffer.data()) == 0); + return std::string(buffer.data()); +} + +static void RequireStored(Pthread thread, const std::string& name) { + Require(ReadName(thread) == name); +} + +int main() { + const Pthread self = pthread_self_nid_postfix(); + RequireStored(self, ""); + + Require(pthread_setname_np_nid_postfix(self, "webkit") == 0); + RequireStored(self, "webkit"); + + Require(pthread_setname_np_nid_postfix(self, "") == 0); + RequireStored(self, ""); + + Require(pthread_setname_np_nid_postfix(self, nullptr) == GUEST_EINVAL); + Require(pthread_setname_np_nid_postfix(nullptr, "name") == GUEST_EINVAL); + + const std::string longest(31, 'b'); + Require(pthread_setname_np_nid_postfix(self, longest.c_str()) == 0); + RequireStored(self, longest); + + const std::string tooLong(32, 'c'); + Require(pthread_setname_np_nid_postfix(self, tooLong.c_str()) == GUEST_ENAMETOOLONG); + RequireStored(self, longest); + return 0; +} diff --git a/core/libs/tests/GuestPosixWrap.cpp b/core/libs/tests/GuestPosixWrap.cpp new file mode 100644 index 0000000000..5eff3c0b04 --- /dev/null +++ b/core/libs/tests/GuestPosixWrap.cpp @@ -0,0 +1,80 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/RemoveTree.hpp" +#include "prx/libSceLibcInternal/include/Api.hpp" +#include "prx/libScePosixForWebKit/include/Api.hpp" +#include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr int Enoent = 2; +constexpr int Ebadf = 9; +constexpr int Eexist = 17; +constexpr int Einval = 22; +constexpr std::uint32_t Unchanged = 0xffffffffu; + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "WebKit wrapper check failed at line %u (guest errno %d)\n", location.line(), *__error_nid_postfix()); + std::abort(); + } +} + +bool Fails(std::int64_t result, int error) { + return result == -1 && *__error_nid_postfix() == error; +} + +} + +int main() { + const auto root = std::filesystem::path("anyps5-posix-wrap-test-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const auto file = (root / "data.bin").string(); + std::ofstream(file, std::ios::binary) << "abcdefgh"; + const auto missing = (root / "missing").string(); + + const int descriptor = open_nid_postfix(file.c_str(), 0, 0); + Require(descriptor >= 0); + Require(__wrap_fchown(descriptor, Unchanged, Unchanged) == 0); + Require(close_nid_postfix(descriptor) == 0); + Require(Fails(__wrap_fchown(-1, Unchanged, Unchanged), Ebadf)); + + const auto linked = (root / "linked.bin").string(); + Require(__wrap_link(file.c_str(), linked.c_str()) == 0 && std::filesystem::hard_link_count(file) == 2); + Require(Fails(__wrap_link(file.c_str(), linked.c_str()), Eexist)); + Require(Fails(__wrap_link(missing.c_str(), (root / "other.bin").string().c_str()), Enoent)); + + char target[64] = {}; + Require(Fails(__wrap_readlink(file.c_str(), target, sizeof(target)), Einval)); + Require(Fails(__wrap_readlink(missing.c_str(), target, sizeof(target)), Enoent)); + + LibcStatvfs wrapped{}; + LibcStatvfs plain{}; + Require(__wrap_statvfs(root.string().c_str(), &wrapped) == 0 && statvfs_nid_postfix(root.string().c_str(), &plain) == 0); + Require(wrapped.f_bsize != 0 && wrapped.f_bsize == plain.f_bsize && wrapped.f_namemax == plain.f_namemax && wrapped.f_fsid == plain.f_fsid); + Require(Fails(__wrap_statvfs(missing.c_str(), &wrapped), Enoent)); + + Require(Fails(__wrap_symlink("data.bin", linked.c_str()), Eexist)); + const auto symbolic = (root / "symbolic").string(); + if (__wrap_symlink("data.bin", symbolic.c_str()) == 0) { + FileStat status{}; + Require(__wrap_readlink(symbolic.c_str(), target, sizeof(target)) == 8 && std::memcmp(target, "data.bin", 8) == 0); + Require(lstat_nid_postfix(symbolic.c_str(), &status) == 0 && (status.st_mode & 0170000) == 0120000); + } + + std::error_code error; + RemoveTree(root, error); + return 0; +} diff --git a/core/libs/tests/GuestPsml.cpp b/core/libs/tests/GuestPsml.cpp index b23fb204b6..f0ec1a6ace 100644 --- a/core/libs/tests/GuestPsml.cpp +++ b/core/libs/tests/GuestPsml.cpp @@ -1,11 +1,10 @@ -#include "prx/libScePsml_debug/include/Api.hpp" +#include "prx/libScePsml_debug/libScePsml_debug.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include -#include -#include -#include +#include #include namespace { @@ -13,8 +12,6 @@ namespace { void Require(bool value) { if (!value) std::abort(); } constexpr std::int32_t kErrNotInitialized = static_cast(0x8A810001); -constexpr std::int32_t kErrInvalidArgument = static_cast(0x8A810005); -constexpr std::int32_t kErrNullOutput = static_cast(0x8A810009); using GetDispatchPacket = std::int32_t (APS5_VABI*)(void*, void*, const void*); @@ -43,40 +40,7 @@ void DispatchPacketUntouched(GetDispatchPacket dispatch) { Require(dwords == untouchedDwords); } -template -bool Throws(TCall call, const char* text) { - try { - call(); - } catch (const std::runtime_error& error) { - return std::string(error.what()).find(text) != std::string::npos; - } - return false; -} -struct ContextParam { - std::array bytes{}; - - void Set(const MfsrBlock* blocks, std::uint64_t count, void* shared) { - std::memcpy(bytes.data() + 0x08, &blocks, sizeof(blocks)); - std::memcpy(bytes.data() + 0x10, &count, sizeof(count)); - std::memcpy(bytes.data() + 0x38, &shared, sizeof(shared)); - } - - void SetInputDimensions(std::uint16_t width, std::uint16_t height) { - std::memcpy(bytes.data() + 0x18, &width, sizeof(width)); - std::memcpy(bytes.data() + 0x1a, &height, sizeof(height)); - } - - void SetDescriptor0(const void* descriptor) { - std::memcpy(bytes.data() + 0x20, &descriptor, sizeof(descriptor)); - } -}; - -std::vector Blocks(const MfsrRequirement& requirement, std::uint64_t first) { - std::vector blocks(requirement.blockCount); - for (std::size_t i = 0; i < blocks.size(); ++i) blocks[i] = {first + i * requirement.blockBytes, requirement.blockBytes}; - return blocks; -} void SetupBeforeInit() { std::uint64_t storage = 0; @@ -102,124 +66,65 @@ void SetupBeforeInit() { Require(scePsmlMfsrIsCaptureInProgress(sentinel) == kErrNotInitialized); } -void SetupAfterInit() { - Require(scePsmlMfsrInit() == 0); - Require(scePsmlMfsrInit() == 0); - - MfsrSharedResourcesParam sharedParam{}; - MfsrRequirement shared{}; - Require(scePsmlMfsrGetSharedResourcesInitRequirement(&shared, &sharedParam) == 0); - Require(shared.blockBytes == 0x200000 && shared.alignment == 0x200000 && shared.blockCount == 52); - - const auto sharedBlocks = Blocks(shared, 0x10000000); - sharedParam.blocks = sharedBlocks.data(); - sharedParam.blockCount = sharedBlocks.size(); - void* sharedResources = nullptr; - Require(scePsmlMfsrCreateSharedResources(&sharedResources, &sharedParam) == 0); - Require(sharedResources != nullptr); - - ContextParam contextParam; - contextParam.Set(nullptr, 0, sharedResources); - MfsrRequirement context{}; - Require(scePsmlMfsrGetContextBufferRequirement800M3_2(&context, reinterpret_cast(contextParam.bytes.data())) == 0); - Require(context.blockBytes == 0x200000 && context.alignment == 0x200000 && context.blockCount == 4); - - const auto contextBlocks = Blocks(context, 0x20000000); - contextParam.Set(contextBlocks.data(), contextBlocks.size(), sharedResources); - void* mfsrContext = nullptr; - Require(scePsmlMfsrCreateContext800M3_2(&mfsrContext, reinterpret_cast(contextParam.bytes.data())) == 0); - Require(mfsrContext != nullptr && mfsrContext != sharedResources); - - std::uint32_t packetDwords = 0; - Require(scePsmlMfsrGetDispatchMfsrPacketSizeInDwords(mfsrContext, &packetDwords) == 0); - Require(packetDwords == 0x600); - Require(scePsmlMfsrGetDispatchMfsrPacketSizeInDwords(mfsrContext, nullptr) == kErrNullOutput); - Require(scePsmlMfsrGetDispatchMfsrPacketSizeInDwords(sharedResources, &packetDwords) == 0); - - std::uint64_t foreign = 0; - Require(scePsmlMfsrGetDispatchMfsrPacketSizeInDwords(&foreign, &packetDwords) == kErrInvalidArgument); - Require(scePsmlMfsrGetDispatchMfsrPacketSizeInDwords(nullptr, &packetDwords) == kErrInvalidArgument); - - float bias = 42.0f; - Require(scePsmlMfsrGetMipmapBias(mfsrContext, &bias, 3840) == kErrInvalidArgument); - Require(bias == 42.0f); - Require(scePsmlMfsrGetMipmapBias(&foreign, &bias, 3840) == kErrInvalidArgument); - Require(scePsmlMfsrGetMipmapBias(sharedResources, &bias, 3840) == kErrInvalidArgument); - - Require(scePsmlMfsrSelectConfig(mfsrContext) == 0); - Require(scePsmlMfsrSelectConfig(&foreign) == kErrInvalidArgument); - Require(scePsmlMfsrRequestCapture(mfsrContext) == 0); - Require(scePsmlMfsrRequestCapture(nullptr) == kErrInvalidArgument); - Require(scePsmlMfsrIsCaptureInProgress(mfsrContext) == 0); - Require(scePsmlMfsrIsCaptureInProgress(&foreign) == kErrInvalidArgument); - - ContextParam dimensionsParam; - dimensionsParam.Set(contextBlocks.data(), contextBlocks.size(), sharedResources); - dimensionsParam.SetInputDimensions(1920, 1080); - void* dimensionContext = nullptr; - Require(scePsmlMfsrCreateContext800M3_2(&dimensionContext, reinterpret_cast(dimensionsParam.bytes.data())) == 0); - Require(scePsmlMfsrGetMipmapBias(dimensionContext, &bias, 3840) == 0); - Require(bias == 0.0f); - Require(scePsmlMfsrGetMipmapBias(dimensionContext, &bias, 1920) == 0); - Require(bias == -1.0f); - Require(scePsmlMfsrGetMipmapBias(dimensionContext, nullptr, 1920) == kErrNullOutput); - Require(scePsmlMfsrReleaseContext(dimensionContext) == 0); - - std::array descriptor{}; - const std::uint16_t descriptorWidth = 1280, descriptorHeight = 720; - std::memcpy(descriptor.data() + 0x20, &descriptorWidth, sizeof(descriptorWidth)); - std::memcpy(descriptor.data() + 0x22, &descriptorHeight, sizeof(descriptorHeight)); - ContextParam descriptorParam; - descriptorParam.Set(contextBlocks.data(), contextBlocks.size(), sharedResources); - descriptorParam.SetDescriptor0(descriptor.data()); - void* descriptorContext = nullptr; - Require(scePsmlMfsrCreateContext800M3_2(&descriptorContext, reinterpret_cast(descriptorParam.bytes.data())) == 0); - Require(scePsmlMfsrGetMipmapBias(descriptorContext, &bias, 2560) == 0); - Require(bias == 0.0f); - Require(scePsmlMfsrReleaseContext(descriptorContext) == 0); - - Require(Throws([&] { scePsmlMfsrReleaseSharedResources(sharedResources); }, "still used by a context")); - Require(Throws([&] { scePsmlMfsrReleaseContext(sharedResources); }, "not created by this library")); - Require(scePsmlMfsrReleaseContext(mfsrContext) == 0); - Require(Throws([&] { scePsmlMfsrReleaseContext(mfsrContext); }, "not created by this library")); - Require(scePsmlMfsrReleaseSharedResources(sharedResources) == 0); +void Mfsr1OnBaseConsole() { + constexpr std::int32_t unsupportedConsole = static_cast(0x8A810012); + Require(scePsmlMfsrInit() == unsupportedConsole); + Require(scePsmlMfsrInit() == unsupportedConsole); + SetupBeforeInit(); } -void RefusedAfterInit() { - MfsrSharedResourcesParam sharedParam{}; - MfsrRequirement requirement{}; - sharedParam.type = 1; - Require(Throws([&] { scePsmlMfsrGetSharedResourcesInitRequirement(&requirement, &sharedParam); }, "type 1 is not supported")); +void ConcurrentCreatesUninitialized() { + std::atomic errors{0}; + std::vector pool; + for (int i = 0; i < 8; ++i) { + pool.emplace_back([&] { + for (int round = 0; round < 64; ++round) { + MfsrSharedResourcesParam sharedParam{}; + void* handle = reinterpret_cast(static_cast(0xdeadbeef)); + if (scePsmlMfsrCreateSharedResources(&handle, &sharedParam) != kErrNotInitialized || handle != reinterpret_cast(static_cast(0xdeadbeef))) ++errors; + } + }); + } + for (auto& worker : pool) worker.join(); + Require(errors == 0); +} - sharedParam.type = 0; - Require(scePsmlMfsrGetSharedResourcesInitRequirement(&requirement, &sharedParam) == 0); - const auto blocks = Blocks(requirement, 0x10000000); - sharedParam.blocks = blocks.data(); - sharedParam.blockCount = 51; - void* sharedResources = nullptr; - Require(Throws([&] { scePsmlMfsrCreateSharedResources(&sharedResources, &sharedParam); }, "fewer memory blocks")); - auto small = blocks; - small[2].bytes = 0x100000; - sharedParam.blocks = small.data(); - sharedParam.blockCount = small.size(); - Require(Throws([&] { scePsmlMfsrCreateSharedResources(&sharedResources, &sharedParam); }, "smaller or less aligned")); - Require(sharedResources == nullptr); +void Mfsr2OnBaseConsole() { + constexpr std::int32_t notInitialized = static_cast(0x8A8A0001); + constexpr std::int32_t unsupportedConsole = static_cast(0x8A8A0012); + Require(scePsmlMfsr2Init() == unsupportedConsole); + Require(scePsmlMfsr2Init() == unsupportedConsole); - std::uint64_t foreign = 0; - ContextParam contextParam; - contextParam.Set(blocks.data(), blocks.size(), &foreign); - void* context = nullptr; - Require(Throws([&] { scePsmlMfsrCreateContext800M3_2(&context, reinterpret_cast(contextParam.bytes.data())); }, "shared resources handle was not created")); - Require(context == nullptr); + std::array param{}; + std::array requirement; + requirement.fill(0xa5); + const auto untouchedRequirement = requirement; + Require(scePsmlMfsr2GetSharedResourcesInitRequirement(reinterpret_cast(requirement.data()), reinterpret_cast(param.data())) == notInitialized); + Require(scePsmlMfsr2GetContextInitRequirement(reinterpret_cast(requirement.data()), reinterpret_cast(param.data())) == notInitialized); + Require(requirement == untouchedRequirement); - std::array param{}; - std::array dwords{}; + std::uint64_t storage = 0; + void* const sentinel = &storage; + void* handle = sentinel; + Require(scePsmlMfsr2CreateSharedResources(&handle, reinterpret_cast(param.data())) == notInitialized); + Require(scePsmlMfsr2CreateContext(&handle, reinterpret_cast(param.data())) == notInitialized); + Require(handle == sentinel); + Require(scePsmlMfsr2ReleaseContext(sentinel) == notInitialized); + Require(scePsmlMfsr2ReleaseSharedResources(sentinel) == notInitialized); + + std::uint32_t packetDwords = 0xa5a5a5a5; + Require(scePsmlMfsr2GetDispatchPacketsSizeInDwords(sentinel, &packetDwords) == notInitialized); + Require(packetDwords == 0xa5a5a5a5); + + std::array dispatchParam{}; + std::array dwords; + dwords.fill(0xa5a5a5a5u); + const auto untouchedDwords = dwords; MfsrCommandBuffer commandBuffer{dwords.data(), static_cast(dwords.size())}; - Require(Throws([&] { scePsmlMfsrGetContextBufferRequirement1100(&requirement, param.data()); }, "not implemented")); - Require(Throws([&] { scePsmlMfsrCreateContext1300(&context, param.data()); }, "not implemented")); - Require(Throws([&] { scePsmlMfsrGetDispatchMfsrPacket900(&foreign, &commandBuffer, param.data()); }, "not implemented")); - Require(Throws([&] { scePsmlMfsrGetDispatchMfsrPacket1000(&foreign, &commandBuffer, param.data()); }, "not implemented")); - Require(Throws([&] { scePsmlMfsrGetDispatchMfsrPacket1300(&foreign, &commandBuffer, param.data()); }, "not implemented")); + Require(scePsmlMfsr2GetDispatchPackets(sentinel, &commandBuffer, reinterpret_cast(dispatchParam.data())) == notInitialized); + Require(commandBuffer.cursor == dwords.data()); + Require(commandBuffer.sizeInDwords == dwords.size()); + Require(dwords == untouchedDwords); } } @@ -266,6 +171,7 @@ int main() { DispatchPacketUntouched(scePsmlMfsrGetDispatchMfsrPacket1000); DispatchPacketUntouched(scePsmlMfsrGetDispatchMfsrPacket1300); SetupBeforeInit(); - SetupAfterInit(); - RefusedAfterInit(); + Mfsr1OnBaseConsole(); + Mfsr2OnBaseConsole(); + ConcurrentCreatesUninitialized(); } diff --git a/core/libs/tests/GuestPthreadAttr.cpp b/core/libs/tests/GuestPthreadAttr.cpp index 5e905c13e9..e15870361b 100644 --- a/core/libs/tests/GuestPthreadAttr.cpp +++ b/core/libs/tests/GuestPthreadAttr.cpp @@ -1,8 +1,10 @@ #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/Api.hpp" #include "SceTypes.hpp" #include #include +#include #include #include #include @@ -19,6 +21,7 @@ static constexpr KernelCpumask CREATION_AFFINITY = 0x3; static constexpr KernelCpumask UPDATED_AFFINITY = 0x1000; static constexpr std::size_t STACK_SIZE = 2u << 20; static constexpr std::size_t MIN_STACK_SIZE = 16384; +static constexpr int GUEST_ESRCH = 3; static constexpr int GUEST_EINVAL = 22; static constexpr int GUEST_ENOTSUP = 45; static constexpr int DETACH_DETACHED = 1; @@ -193,6 +196,8 @@ static void CheckThreadScope() { PthreadAttr queryAttr = nullptr; Require(scePthreadAttrInit(&queryAttr) == SCE_OK); Require(scePthreadAttrGet(thread, &queryAttr) == SCE_OK); + int reportedInherit = -1; + Require(pthread_attr_getinheritsched_nid_postfix(&queryAttr, &reportedInherit) == 0 && reportedInherit == EXPLICIT_SCHED); int scope = -1; Require(pthread_attr_getscope_nid_postfix(&queryAttr, &scope) == 0 && scope == SCOPE_PROCESS); @@ -215,21 +220,28 @@ static void CheckThreadScope() { Require(scePthreadAttrInit(&queryAttr) == SCE_OK); Require(scePthreadAttrGet(inheritedThread, &queryAttr) == SCE_OK); + reportedInherit = -1; + Require(pthread_attr_getinheritsched_nid_postfix(&queryAttr, &reportedInherit) == 0 && reportedInherit == INHERIT_SCHED); scope = -1; Require(pthread_attr_getscope_nid_postfix(&queryAttr, &scope) == 0 && scope == SCOPE_SYSTEM); + Require(pthread_attr_setinheritsched_nid_postfix(&queryAttr, EXPLICIT_SCHED) == 0); Require(pthread_attr_get_np_nid_postfix(inheritedThread, &queryAttr) == 0); Require(pthread_attr_getscope_nid_postfix(&queryAttr, &scope) == 0 && scope == SCOPE_SYSTEM); + reportedInherit = -1; + Require(pthread_attr_getinheritsched_nid_postfix(&queryAttr, &reportedInherit) == 0 && reportedInherit == INHERIT_SCHED); Require(scePthreadAttrDestroy(&queryAttr) == SCE_OK); releaseInherited.set_value(); Require(scePthreadJoin(inheritedThread, nullptr) == SCE_OK); } -static int QueryResult(Pthread thread) { +static int QueryResult(Pthread thread, bool posix = false) { PthreadAttr attr = nullptr; Require(scePthreadAttrInit(&attr) == SCE_OK); Require(scePthreadAttrSetstacksize(&attr, STACK_SIZE) == SCE_OK); - const int result = scePthreadAttrGet(thread, &attr); + *__error_nid_postfix() = 12345; + const int result = posix ? pthread_attr_get_np_nid_postfix(thread, &attr) : scePthreadAttrGet(thread, &attr); + if (posix) Require(*__error_nid_postfix() == 12345); std::size_t stackSize = 0; Require(scePthreadAttrGetstacksize(&attr, &stackSize) == SCE_OK); Require(result == SCE_OK || stackSize == STACK_SIZE); @@ -237,6 +249,13 @@ static int QueryResult(Pthread thread) { return result; } +static void CheckPosixQuery(Pthread thread, int expected) { + const int result = QueryResult(thread, true); + if (result == expected) return; + std::fprintf(stderr, "pthread_attr_get_np returned %d, expected %d\n", result, expected); + std::abort(); +} + static bool ReportedEnded(Pthread thread) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); while (QueryResult(thread) == SCE_OK) { @@ -249,11 +268,13 @@ static bool ReportedEnded(Pthread thread) { struct ExitQuery { PthreadKey key = -1; int result = -1; + int posixResult = -1; }; static void APS5_VABI QuerySelfAtExit(void* value) { auto* query = static_cast(value); query->result = QueryResult(scePthreadSelf()); + query->posixResult = QueryResult(scePthreadSelf(), true); } static void* APS5_VABI SetExitQuery(void* arg) { @@ -268,22 +289,28 @@ static void* APS5_VABI Finish(void*) { static void CheckEndedThreads() { Require(QueryResult(scePthreadSelf()) == SCE_OK); + CheckPosixQuery(scePthreadSelf(), 0); + CheckPosixQuery(nullptr, GUEST_EINVAL); Pthread joined = nullptr; Require(scePthreadCreate(&joined, nullptr, Finish, nullptr, nullptr) == SCE_OK); Require(scePthreadJoin(joined, nullptr) == SCE_OK); Require(QueryResult(joined) == SCE_KERNEL_ERROR_ESRCH); + CheckPosixQuery(joined, GUEST_ESRCH); PthreadAttr unset = nullptr; Require(scePthreadAttrGet(joined, nullptr) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadAttrGet(joined, &unset) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_attr_get_np_nid_postfix(joined, nullptr) == GUEST_EINVAL); + Require(pthread_attr_get_np_nid_postfix(joined, &unset) == GUEST_EINVAL); ExitQuery exitQuery; Require(scePthreadKeyCreate(&exitQuery.key, reinterpret_cast(QuerySelfAtExit)) == SCE_OK); Pthread finished = nullptr; Require(scePthreadCreate(&finished, nullptr, SetExitQuery, &exitQuery, nullptr) == SCE_OK); Require(ReportedEnded(finished)); + CheckPosixQuery(finished, GUEST_ESRCH); Require(scePthreadJoin(finished, nullptr) == SCE_OK); - Require(exitQuery.result == SCE_OK); + Require(exitQuery.result == SCE_OK && exitQuery.posixResult == 0); Require(QueryResult(finished) == SCE_KERNEL_ERROR_ESRCH); Require(scePthreadKeyDelete(exitQuery.key) == SCE_OK); @@ -294,6 +321,7 @@ static void CheckEndedThreads() { Pthread detached = nullptr; Require(scePthreadCreate(&detached, &detachedAttr, Finish, nullptr, nullptr) == SCE_OK); Require(ReportedEnded(detached)); + CheckPosixQuery(detached, GUEST_ESRCH); } Require(scePthreadAttrDestroy(&detachedAttr) == SCE_OK); } diff --git a/core/libs/tests/GuestPthreadAttrErrors.cpp b/core/libs/tests/GuestPthreadAttrErrors.cpp index 97636eb722..2c07d90319 100644 --- a/core/libs/tests/GuestPthreadAttrErrors.cpp +++ b/core/libs/tests/GuestPthreadAttrErrors.cpp @@ -64,9 +64,7 @@ int main() { Require(scePthreadAttrGetdetachstate(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadAttrGetschedparam(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadAttrGetschedpolicy(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); - bool threw = false; - try { scePthreadAttrGetinheritsched(&attr, nullptr); } catch (const std::runtime_error&) { threw = true; } - Require(threw); + Require(scePthreadAttrGetinheritsched(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); for (PthreadAttr* missing : {static_cast(nullptr), static_cast(&attr)}) { PthreadAttr unset = nullptr; diff --git a/core/libs/tests/GuestPthreadCondSetname.cpp b/core/libs/tests/GuestPthreadCondSetname.cpp new file mode 100644 index 0000000000..7d508abebb --- /dev/null +++ b/core/libs/tests/GuestPthreadCondSetname.cpp @@ -0,0 +1,39 @@ +#include "prx/libkernel/include/Api.hpp" +#include "SceTypes.hpp" +#include +#include + +static constexpr int GuestEinval = 22; + +static void Require(bool value) { if (!value) std::abort(); } + +template +static bool ThrowsInvalidArgument(TCall call) { + try { + call(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + +int main() { + PthreadCond cond = nullptr; + Require(pthread_cond_init_nid_postfix(&cond, nullptr) == 0); + Require(pthread_cond_setname_np_nid_postfix(&cond, "first") == 0); + Require(pthread_cond_setname_np_nid_postfix(&cond, "second") == 0); + Require(pthread_cond_signal_nid_postfix(&cond) == 0); + Require(ThrowsInvalidArgument([&] { return pthread_cond_setname_np_nid_postfix(&cond, nullptr); })); + Require(ThrowsInvalidArgument([] { return pthread_cond_setname_np_nid_postfix(nullptr, "cond"); })); + Require(pthread_cond_destroy_nid_postfix(&cond) == 0); + Require(pthread_cond_setname_np_nid_postfix(&cond, "destroyed") == GuestEinval); + Require(pthread_cond_init_nid_postfix(&cond, nullptr) == 0); + Require(pthread_cond_setname_np_nid_postfix(&cond, "reinitialized") == 0); + Require(pthread_cond_destroy_nid_postfix(&cond) == 0); + + PthreadCond lazy = nullptr; + Require(pthread_cond_setname_np_nid_postfix(&lazy, "lazy") == 0 && lazy != nullptr); + Require(pthread_cond_signal_nid_postfix(&lazy) == 0); + Require(pthread_cond_destroy_nid_postfix(&lazy) == 0); + Require(pthread_cond_setname_np_nid_postfix(&lazy, "lazy") == GuestEinval); +} diff --git a/core/libs/tests/GuestPthreadCondattr.cpp b/core/libs/tests/GuestPthreadCondattr.cpp index fb96e05b2d..208bbb7776 100644 --- a/core/libs/tests/GuestPthreadCondattr.cpp +++ b/core/libs/tests/GuestPthreadCondattr.cpp @@ -1,4 +1,5 @@ #include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/Api.hpp" #include "prx/libkernel/Pthread/include/Cond.hpp" #include "SceTypes.hpp" #include @@ -12,8 +13,32 @@ static constexpr KernelClockid Monotonic = 4; static void Require(bool value) { if (!value) std::abort(); } +static void CheckClock(const PthreadCondattr* attr, KernelClockid expected) { + const auto handle = *attr; + KernelClockid output[] = {0x12345678, -1, 0x23456789}; + *__error_nid_postfix() = 123; + Require(pthread_condattr_getclock_nid_postfix(attr, &output[1]) == 0); + Require(output[0] == 0x12345678 && output[1] == expected && output[2] == 0x23456789); + Require(*attr == handle && *__error_nid_postfix() == 123); +} + +static void CheckClockRefused(const PthreadCondattr* attr, KernelClockid* clockId) { + bool threw = false; + try { + pthread_condattr_getclock_nid_postfix(attr, clockId); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); +} + int main() { PthreadCondattr unset = nullptr; + KernelClockid clockId = -1; + CheckClockRefused(nullptr, &clockId); + Require(clockId == -1); + CheckClockRefused(&unset, &clockId); + Require(clockId == -1); int sharing = -1; Require(scePthreadCondattrSetclock(nullptr, Monotonic) == SceEinval); Require(scePthreadCondattrSetclock(&unset, Monotonic) == SceEinval); @@ -29,14 +54,34 @@ int main() { PthreadCondattr attr = nullptr; Require(scePthreadCondattrInit(&attr) == SCE_OK); - for (const KernelClockid clock : {0, 1, 2, Monotonic, 5, 12}) { + CheckClock(&attr, 0); + CheckClockRefused(&attr, nullptr); + CheckClock(&attr, 0); + PthreadCondattr other = nullptr; + Require(pthread_condattr_init_nid_postfix(&other) == 0); + for (const KernelClockid clock : {0, 1, 2, Monotonic}) { Require(scePthreadCondattrSetclock(&attr, clock) == SCE_OK); + CheckClock(&attr, clock); + CheckClock(&other, 0); Require(pthread_condattr_setclock_nid_postfix(&attr, clock) == 0); + CheckClock(&attr, clock); } + Require(pthread_condattr_setclock_nid_postfix(&other, Monotonic) == 0); + CheckClock(&other, Monotonic); + CheckClock(&attr, Monotonic); + Require(pthread_condattr_destroy_nid_postfix(&other) == 0 && other == nullptr); + CheckClockRefused(&other, &clockId); + Require(clockId == -1); Require(scePthreadCondattrSetpshared(&attr, 1) == SceEinval); Require(scePthreadCondattrSetpshared(&attr, 0) == SCE_OK); Require(scePthreadCondattrGetpshared(&attr, &sharing) == SCE_OK && sharing == 0); Require(pthread_condattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); + for (const KernelClockid clock : {-1, 3, 5, 12, 13, 14, 15}) { + Require(scePthreadCondattrSetclock(&attr, clock) == SceEinval); + CheckClock(&attr, Monotonic); + Require(pthread_condattr_setclock_nid_postfix(&attr, clock) == GuestEinval); + CheckClock(&attr, Monotonic); + } for (const int invalid : {1, 2, -1}) { Require(pthread_condattr_setpshared_nid_postfix(&attr, invalid) == GuestEinval); sharing = -1; @@ -54,6 +99,8 @@ int main() { } Require(threw); Require(scePthreadCondattrDestroy(&attr) == SCE_OK && attr == nullptr); + CheckClockRefused(&attr, &clockId); + Require(clockId == -1); Require(scePthreadCondattrDestroy(&attr) == SceEinval); sharing = -1; Require(pthread_condattr_getpshared_nid_postfix(&attr, &sharing) == GuestEinval && sharing == -1); diff --git a/core/libs/tests/GuestPthreadCreateSuspended.cpp b/core/libs/tests/GuestPthreadCreateSuspended.cpp new file mode 100644 index 0000000000..cadde5c8e2 --- /dev/null +++ b/core/libs/tests/GuestPthreadCreateSuspended.cpp @@ -0,0 +1,276 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel_sys/include/Api.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016u); +static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(0x80020003u); +static constexpr int GUEST_ESRCH = 3; +static constexpr int GUEST_EINVAL = 22; +static constexpr int GUEST_SIGUSR1 = 30; +static constexpr int GUEST_CREATE_DETACHED = 1; +static constexpr int DETACHED_ROUNDS = 200; +static constexpr auto SUSPENDED_WINDOW = std::chrono::milliseconds(50); +static void* const CANCELED = reinterpret_cast(std::uintptr_t{1}); +static void* const FINISHED = reinterpret_cast(std::uintptr_t{7}); + +static void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +struct Started { + std::atomic ran{false}; + std::atomic returned{false}; +}; + +static std::atomic release{false}; +static std::atomic stopResuming{false}; +static std::atomic raised{0}; +static std::atomic raisedOn{nullptr}; + +static void* APS5_VABI Record(void* opaque) { + static_cast(opaque)->ran.store(true); + return FINISHED; +} + +static void* APS5_VABI CancelAfterResume(void* opaque) { + auto* started = static_cast(opaque); + started->ran.store(true); + scePthreadTestcancel(); + started->returned.store(true); + return FINISHED; +} + +static void* APS5_VABI CancelInResumeAll(void* opaque) { + auto* started = static_cast(opaque); + started->ran.store(true); + Require(scePthreadCancel(scePthreadSelf()) == SCE_OK, "scePthreadCancel of the calling thread failed"); + pthread_resume_all_np_nid_postfix(); + started->returned.store(true); + return FINISHED; +} + +static void* APS5_VABI ResumeAllUntilStopped(void*) { + while (!stopResuming.load()) pthread_resume_all_np_nid_postfix(); + return FINISHED; +} + +static void* APS5_VABI WaitForRelease(void*) { + while (!release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + return FINISHED; +} + +static void APS5_VABI Handler(int, void*) { + raisedOn.store(scePthreadSelf()); + raised.fetch_add(1); +} + +static void CreateSuspendedPosix(Pthread* thread, PthreadEntry entry, void* arg) { + PthreadAttr attr = nullptr; + Require(pthread_attr_init_nid_postfix(&attr) == 0, "pthread_attr_init failed"); + Require(pthread_attr_setcreatesuspend_np_nid_postfix(&attr) == 0, "pthread_attr_setcreatesuspend_np failed"); + Require(pthread_create_nid_postfix(thread, &attr, entry, arg) == 0, "pthread_create of a suspended thread failed"); + Require(pthread_attr_destroy_nid_postfix(&attr) == 0, "pthread_attr_destroy failed"); +} + +static void CreateSuspendedSce(Pthread* thread, PthreadEntry entry, void* arg) { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK, "scePthreadAttrInit failed"); + Require(scePthreadAttrSetcreatesuspend(&attr) == SCE_OK, "scePthreadAttrSetcreatesuspend failed"); + Require(scePthreadCreate(thread, &attr, entry, arg, "suspended") == SCE_OK, "scePthreadCreate of a suspended thread failed"); + Require(scePthreadAttrDestroy(&attr) == SCE_OK, "scePthreadAttrDestroy failed"); +} + +static void ReportsErrors() { + Require(pthread_attr_setcreatesuspend_np_nid_postfix(nullptr) == GUEST_EINVAL, "pthread_attr_setcreatesuspend_np accepted a null attribute"); + Require(scePthreadAttrSetcreatesuspend(nullptr) == SCE_KERNEL_ERROR_EINVAL, "scePthreadAttrSetcreatesuspend accepted a null attribute"); + PthreadAttr destroyed = nullptr; + Require(scePthreadAttrInit(&destroyed) == SCE_OK, "scePthreadAttrInit failed"); + Require(scePthreadAttrDestroy(&destroyed) == SCE_OK, "scePthreadAttrDestroy failed"); + Require(pthread_attr_setcreatesuspend_np_nid_postfix(&destroyed) == GUEST_EINVAL, "pthread_attr_setcreatesuspend_np accepted a destroyed attribute"); + Require(scePthreadAttrSetcreatesuspend(&destroyed) == SCE_KERNEL_ERROR_EINVAL, "scePthreadAttrSetcreatesuspend accepted a destroyed attribute"); + Require(pthread_resume_np_nid_postfix(nullptr) == GUEST_EINVAL, "pthread_resume_np accepted a null thread"); + Require(scePthreadResume(nullptr) == SCE_KERNEL_ERROR_EINVAL, "scePthreadResume accepted a null thread"); + Pthread joined = nullptr; + Started started; + Require(scePthreadCreate(&joined, nullptr, Record, &started, "joined") == SCE_OK, "scePthreadCreate failed"); + Require(scePthreadJoin(joined, nullptr) == SCE_OK, "scePthreadJoin failed"); + Require(pthread_resume_np_nid_postfix(joined) == GUEST_ESRCH, "pthread_resume_np found a joined thread"); + Require(scePthreadResume(joined) == SCE_KERNEL_ERROR_ESRCH, "scePthreadResume found a joined thread"); +} + +static void ResumesOneThread(bool sce) { + Started started; + Pthread thread = nullptr; + if (sce) CreateSuspendedSce(&thread, Record, &started); + else CreateSuspendedPosix(&thread, Record, &started); + std::this_thread::sleep_for(SUSPENDED_WINDOW); + Require(!started.ran.load(), "a thread created suspended ran before it was resumed"); + Require((sce ? scePthreadResume(thread) : pthread_resume_np_nid_postfix(thread)) == 0, "resuming a suspended thread failed"); + void* result = nullptr; + Require((sce ? scePthreadJoin(thread, &result) : pthread_join_nid_postfix(thread, &result)) == 0, "joining a resumed thread failed"); + Require(result == FINISHED && started.ran.load(), "a resumed thread did not run its start routine"); +} + +static void ResumesRunningAndCallingThreads() { + Require(pthread_resume_np_nid_postfix(scePthreadSelf()) == 0, "pthread_resume_np of the calling thread failed"); + release.store(false); + Pthread running = nullptr; + Require(scePthreadCreate(&running, nullptr, WaitForRelease, nullptr, "running") == SCE_OK, "scePthreadCreate failed"); + Require(pthread_resume_np_nid_postfix(running) == 0, "pthread_resume_np of a running thread failed"); + release.store(true); + void* result = nullptr; + Require(scePthreadJoin(running, &result) == SCE_OK && result == FINISHED, "joining a running thread failed"); +} + +static void ResumesAllThreads(void (*resumeAll)()) { + Started first; + Started second; + Pthread firstThread = nullptr; + Pthread secondThread = nullptr; + CreateSuspendedPosix(&firstThread, Record, &first); + CreateSuspendedSce(&secondThread, Record, &second); + std::this_thread::sleep_for(SUSPENDED_WINDOW); + Require(!first.ran.load() && !second.ran.load(), "a thread created suspended ran before it was resumed"); + resumeAll(); + void* result = nullptr; + Require(scePthreadJoin(firstThread, &result) == SCE_OK && result == FINISHED && first.ran.load(), "resuming all threads left the first one suspended"); + Require(scePthreadJoin(secondThread, &result) == SCE_OK && result == FINISHED && second.ran.load(), "resuming all threads left the second one suspended"); +} + +static void CancelsBeforeResume() { + Started started; + Pthread thread = nullptr; + CreateSuspendedSce(&thread, CancelAfterResume, &started); + Require(scePthreadCancel(thread) == SCE_OK, "scePthreadCancel of a suspended thread failed"); + Require(pthread_resume_np_nid_postfix(thread) == 0, "pthread_resume_np of a canceled thread failed"); + void* result = nullptr; + Require(scePthreadJoin(thread, &result) == SCE_OK, "joining a canceled thread failed"); + Require(result == CANCELED && started.ran.load() && !started.returned.load(), "a cancel requested while suspended did not take effect at the first cancellation point"); +} + +static void CancelsInResumeAll() { + Started started; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, CancelInResumeAll, &started, "resume_all") == SCE_OK, "scePthreadCreate failed"); + void* result = nullptr; + Require(scePthreadJoin(thread, &result) == SCE_OK, "joining a canceled thread failed"); + Require(result == CANCELED && started.ran.load() && !started.returned.load(), "pthread_resume_all_np did not act on a pending cancel"); +} + +static void ReportsCreateSuspendedInAttrGet(int (APS5_VABI* get)(Pthread, PthreadAttr*)) { + Started first; + Started second; + Pthread suspended = nullptr; + CreateSuspendedPosix(&suspended, Record, &first); + PthreadAttr attr = nullptr; + Require(pthread_attr_init_nid_postfix(&attr) == 0, "pthread_attr_init failed"); + Require(get(suspended, &attr) == 0, "getting the attributes of a suspended thread failed"); + Pthread copy = nullptr; + Require(pthread_create_nid_postfix(©, &attr, Record, &second) == 0, "pthread_create with the attributes of a suspended thread failed"); + std::this_thread::sleep_for(SUSPENDED_WINDOW); + Require(!second.ran.load(), "a thread created with the attributes of a suspended thread ran before it was resumed"); + Require(pthread_resume_np_nid_postfix(copy) == 0 && pthread_resume_np_nid_postfix(suspended) == 0, "resuming a suspended thread failed"); + void* result = nullptr; + Require(pthread_join_nid_postfix(copy, &result) == 0 && result == FINISHED && second.ran.load(), "joining a resumed thread failed"); + Require(pthread_join_nid_postfix(suspended, &result) == 0 && result == FINISHED && first.ran.load(), "joining a resumed thread failed"); + release.store(false); + Pthread running = nullptr; + Require(scePthreadCreate(&running, nullptr, WaitForRelease, nullptr, "running") == SCE_OK, "scePthreadCreate failed"); + Require(pthread_attr_setcreatesuspend_np_nid_postfix(&attr) == 0, "pthread_attr_setcreatesuspend_np failed"); + Require(get(running, &attr) == 0, "getting the attributes of a running thread failed"); + Started third; + Pthread plain = nullptr; + Require(pthread_create_nid_postfix(&plain, &attr, Record, &third) == 0, "pthread_create with the attributes of a running thread failed"); + Require(pthread_join_nid_postfix(plain, &result) == 0 && result == FINISHED && third.ran.load(), "a thread created with the attributes of a running thread did not run"); + release.store(true); + Require(scePthreadJoin(running, nullptr) == SCE_OK, "joining a running thread failed"); + Require(pthread_attr_destroy_nid_postfix(&attr) == 0, "pthread_attr_destroy failed"); +} + +static void ResumesDetachedThreadsWhileTheyExit() { + stopResuming.store(false); + Pthread resumer = nullptr; + Require(scePthreadCreate(&resumer, nullptr, ResumeAllUntilStopped, nullptr, "resumer") == SCE_OK, "scePthreadCreate failed"); + for (int round = 0; round < DETACHED_ROUNDS; ++round) { + Started started; + PthreadAttr attr = nullptr; + Require(pthread_attr_init_nid_postfix(&attr) == 0, "pthread_attr_init failed"); + Require(pthread_attr_setcreatesuspend_np_nid_postfix(&attr) == 0, "pthread_attr_setcreatesuspend_np failed"); + Require(pthread_attr_setdetachstate_nid_postfix(&attr, GUEST_CREATE_DETACHED) == 0, "pthread_attr_setdetachstate failed"); + Pthread thread = nullptr; + Require(pthread_create_nid_postfix(&thread, &attr, Record, &started) == 0, "pthread_create of a detached suspended thread failed"); + Require(pthread_attr_destroy_nid_postfix(&attr) == 0, "pthread_attr_destroy failed"); + int result = 0; + while ((result = pthread_resume_np_nid_postfix(thread)) == 0) std::this_thread::yield(); + Require(result == GUEST_ESRCH && started.ran.load(), "a detached thread resumed while it exited did not run its start routine"); + } + stopResuming.store(true); + Require(scePthreadJoin(resumer, nullptr) == SCE_OK, "joining the resuming thread failed"); +} + +#ifdef _WIN32 +static void RefusesRaiseWhileSuspended() { + Require(sceKernelInstallExceptionHandler(GUEST_SIGUSR1, reinterpret_cast(&Handler)) == SCE_OK, "sceKernelInstallExceptionHandler failed"); + Started started; + Pthread thread = nullptr; + CreateSuspendedSce(&thread, Record, &started); + bool refused = false; + try { + sceKernelRaiseException(thread, GUEST_SIGUSR1); + } catch (const std::runtime_error& error) { + refused = std::string(error.what()).find("suspended") != std::string::npos; + } + Require(refused, "sceKernelRaiseException did not refuse a thread still suspended at creation"); + Require(pthread_resume_np_nid_postfix(thread) == 0, "pthread_resume_np failed"); + Require(scePthreadJoin(thread, nullptr) == SCE_OK && started.ran.load(), "joining a resumed thread failed"); + Require(raised.load() == 0, "the handler ran for a refused raise"); + Require(sceKernelRemoveExceptionHandler(GUEST_SIGUSR1) == SCE_OK, "sceKernelRemoveExceptionHandler failed"); +} +#else +static void DefersRaiseUntilResume() { + Require(sceKernelInstallExceptionHandler(GUEST_SIGUSR1, reinterpret_cast(&Handler)) == SCE_OK, "sceKernelInstallExceptionHandler failed"); + Started started; + Pthread thread = nullptr; + CreateSuspendedSce(&thread, Record, &started); + Require(sceKernelRaiseException(thread, GUEST_SIGUSR1) == SCE_OK, "sceKernelRaiseException on a suspended thread failed"); + std::this_thread::sleep_for(SUSPENDED_WINDOW); + Require(raised.load() == 0, "the handler ran on a thread still suspended at creation"); + Require(pthread_resume_np_nid_postfix(thread) == 0, "pthread_resume_np failed"); + Require(scePthreadJoin(thread, nullptr) == SCE_OK && started.ran.load(), "joining a resumed thread failed"); + Require(raised.load() == 1 && raisedOn.load() == thread, "the handler did not run on the thread once it was resumed"); + Require(sceKernelRemoveExceptionHandler(GUEST_SIGUSR1) == SCE_OK, "sceKernelRemoveExceptionHandler failed"); +} +#endif + +int main() { + ReportsErrors(); + ResumesOneThread(false); + ResumesOneThread(true); + ResumesRunningAndCallingThreads(); + ResumesAllThreads([] { Require(pthread_multi_np_nid_postfix() == 0, "pthread_multi_np failed"); }); + ResumesAllThreads([] { pthread_resume_all_np_nid_postfix(); }); + ResumesAllThreads([] { Require(scePthreadMulti() == SCE_OK, "scePthreadMulti failed"); }); + ResumesAllThreads([] { scePthreadResumeAll(); }); + CancelsBeforeResume(); + CancelsInResumeAll(); + ReportsCreateSuspendedInAttrGet(pthread_attr_get_np_nid_postfix); + ReportsCreateSuspendedInAttrGet(scePthreadAttrGet); + ResumesDetachedThreadsWhileTheyExit(); +#ifdef _WIN32 + RefusesRaiseWhileSuspended(); +#else + DefersRaiseUntilResume(); +#endif +} diff --git a/core/libs/tests/GuestPthreadDestroyedObjects.cpp b/core/libs/tests/GuestPthreadDestroyedObjects.cpp index 3232f4c87e..2687a84b66 100644 --- a/core/libs/tests/GuestPthreadDestroyedObjects.cpp +++ b/core/libs/tests/GuestPthreadDestroyedObjects.cpp @@ -39,10 +39,19 @@ int main() { PthreadCond cond = nullptr; Require(scePthreadCondInit(&cond, nullptr, "destroyed") == SCE_OK); Require(scePthreadCondDestroy(&cond) == SCE_OK); + Require(reinterpret_cast(cond) == 1); Require(scePthreadCondSignal(&cond) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadCondBroadcast(&cond) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadCondWait(&cond, &mutex) == SCE_KERNEL_ERROR_EINVAL); Require(pthread_cond_signal_nid_postfix(&cond) == POSIX_EINVAL); Require(scePthreadCondDestroy(&cond) == SCE_KERNEL_ERROR_EINVAL); Require(pthread_cond_destroy_nid_postfix(&cond) == POSIX_EINVAL); + + PthreadCond untouched = nullptr; + Require(scePthreadCondDestroy(&untouched) == SCE_OK && untouched == nullptr); + Require(scePthreadCondSignal(&untouched) == SCE_OK && untouched != nullptr); + Require(scePthreadCondDestroy(&untouched) == SCE_OK && reinterpret_cast(untouched) == 1); + untouched = nullptr; + Require(scePthreadCondBroadcast(&untouched) == SCE_OK && untouched != nullptr); + Require(scePthreadCondDestroy(&untouched) == SCE_OK); } diff --git a/core/libs/tests/GuestPthreadMutexErrors.cpp b/core/libs/tests/GuestPthreadMutexErrors.cpp index ed2b0eb297..7d7947f824 100644 --- a/core/libs/tests/GuestPthreadMutexErrors.cpp +++ b/core/libs/tests/GuestPthreadMutexErrors.cpp @@ -47,7 +47,7 @@ static void CheckOwnershipAndRelativeLock() { Require(pthread_mutex_isowned_np_nid_postfix(&adaptive) == 0 && adaptive == AdaptiveInitializer); Require(pthread_mutex_reltimedlock_np_nid_postfix(&mutex, 1000) == 0 && mutex != nullptr); Require(pthread_mutex_isowned_np_nid_postfix(&mutex) == 1); - Require(pthread_mutex_reltimedlock_np_nid_postfix(&mutex, 1000) == POSIX_EDEADLK); + Require(pthread_mutex_reltimedlock_np_nid_postfix(&mutex, 1000) == POSIX_ETIMEDOUT); OtherThread other{&mutex}; Pthread thread = nullptr; Require(scePthreadCreate(&thread, nullptr, CheckFromOtherThread, &other, nullptr) == SCE_OK); diff --git a/core/libs/tests/GuestPthreadRwlock.cpp b/core/libs/tests/GuestPthreadRwlock.cpp index 937edc4b09..1f6783b2d5 100644 --- a/core/libs/tests/GuestPthreadRwlock.cpp +++ b/core/libs/tests/GuestPthreadRwlock.cpp @@ -182,7 +182,7 @@ static void CheckFailedLockAttempts() { PthreadRwlock rwlock = nullptr; Require(pthread_rwlock_wrlock_nid_postfix(&rwlock) == 0); const KernelTimespec timeout = After(10); - Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, &timeout) == GUEST_EDEADLK); + Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, &timeout) == GUEST_ETIMEDOUT); Require(pthread_rwlock_tryrdlock_nid_postfix(&rwlock) == GUEST_EBUSY); Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); @@ -447,9 +447,10 @@ int main() { Require(pthread_rwlock_trywrlock_nid_postfix(&rwlock) == 0); Require(pthread_rwlock_tryrdlock_nid_postfix(&rwlock) == GUEST_EBUSY); Require(pthread_rwlock_trywrlock_nid_postfix(&rwlock) == GUEST_EBUSY); - KernelTimespec deadline = After(5000); - Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, &deadline) == GUEST_EDEADLK); - Require(pthread_rwlock_timedwrlock_nid_postfix(&rwlock, &deadline) == GUEST_EDEADLK); + KernelTimespec deadline = After(20); + Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, &deadline) == GUEST_ETIMEDOUT); + deadline = After(20); + Require(pthread_rwlock_timedwrlock_nid_postfix(&rwlock, &deadline) == GUEST_ETIMEDOUT); Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == GUEST_EPERM); Require(pthread_rwlock_tryrdlock_nid_postfix(&rwlock) == 0); diff --git a/core/libs/tests/GuestRaiseInSystemCall.cpp b/core/libs/tests/GuestRaiseInSystemCall.cpp index f1b75ad913..6f0d08c8be 100644 --- a/core/libs/tests/GuestRaiseInSystemCall.cpp +++ b/core/libs/tests/GuestRaiseInSystemCall.cpp @@ -5,75 +5,118 @@ #include #include #include +#include #include #include #include +#include #include #include static constexpr int SigUsr1 = 30; static constexpr int Raises = 10; static constexpr DWORD PipeBytes = 4096; +static constexpr std::uint64_t BlockBytes = 4u * PipeBytes; +static constexpr std::size_t RipOffset = 0xe0; +static constexpr ULONG ThreadSuspendCount = 35; static void Require(bool value) { if (!value) std::abort(); } static std::FILE* shared = nullptr; static std::atomic deliveries{0}; static std::atomic released{0}; -static std::atomic stop{false}; -static std::atomic blocks{0}; +static std::atomic written{0}; +static std::atomic blocked{0}; +static std::atomic writerThread{nullptr}; +static bool delayReturn = false; -static void APS5_VABI Handler(int, void*) { +static bool InTestImage(std::uint64_t rip) { + const auto base = reinterpret_cast(GetModuleHandleW(nullptr)); + const auto* dos = reinterpret_cast(base); + const auto* nt = reinterpret_cast(base + dos->e_lfanew); + return rip >= base && rip - base < nt->OptionalHeader.SizeOfImage; +} + +static bool Suspended(HANDLE thread) { + using Query = LONG(NTAPI*)(HANDLE, ULONG, void*, ULONG, ULONG*); + static const auto query = reinterpret_cast(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQueryInformationThread")); + ULONG count = 0; + Require(query != nullptr && query(thread, ThreadSuspendCount, &count, sizeof(count), nullptr) >= 0); + return count > 0; +} + +static void APS5_VABI Handler(int, void* context) { const auto delivery = deliveries.fetch_add(1) + 1; + std::uint64_t rip = 0; + std::memcpy(&rip, static_cast(context) + RipOffset, sizeof(rip)); + Require(InTestImage(rip)); while (released.load() < delivery) std::this_thread::yield(); } static void* APS5_VABI Writer(void*) { - const std::string block(16u * PipeBytes, 'x'); - while (!stop.load()) { + HANDLE self = nullptr; + Require(DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &self, THREAD_QUERY_INFORMATION, FALSE, 0) != 0); + writerThread.store(self); + const std::string block(BlockBytes, 'x'); + for (int raise = 1; raise <= Raises; ++raise) { Require(std::fwrite(block.data(), 1, block.size(), shared) == block.size()); - blocks.fetch_add(1); - volatile std::uint64_t spin = 0; - for (std::uint64_t i = 0; i < 200000; ++i) spin = spin + i; + if (delayReturn) { + volatile std::uint64_t spin = 0; + for (std::uint64_t i = 0; i < 100000000; ++i) spin = spin + i; + } + written.store(raise); + while (deliveries.load() < raise) { + } } return nullptr; } -int main() { +int main(int argc, char** argv) { + const std::string_view mode = argc > 1 ? argv[1] : ""; + Require(mode.empty() || mode == "--delay-raise" || mode == "--delay-return"); + delayReturn = mode == "--delay-return"; std::atomic done{false}; std::thread watchdog([&done] { - for (int i = 0; i < 25000 && !done.load(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(25); + while (!done.load() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(10)); if (!done.load()) std::abort(); }); HANDLE read = nullptr; HANDLE write = nullptr; Require(CreatePipe(&read, &write, nullptr, PipeBytes) != 0); shared = _fdopen(_open_osfhandle(reinterpret_cast(write), _O_WRONLY | _O_BINARY), "wb"); - Require(shared != nullptr); - std::atomic draining{true}; + Require(shared != nullptr && std::setvbuf(shared, nullptr, _IONBF, 0) == 0); std::thread reader([&] { char chunk[PipeBytes]; DWORD got = 0; + std::uint64_t total = 0; + int block = 0; while (ReadFile(read, chunk, sizeof(chunk), &got, nullptr) != 0 && got != 0) { - if (draining.load()) std::this_thread::sleep_for(std::chrono::milliseconds(10)); + total += got; + if (block == Raises || total <= static_cast(block) * (BlockBytes + 1)) continue; + blocked.store(++block); + while (!Suspended(writerThread.load())) { + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Require(written.load() < block); } }); Require(sceKernelInstallExceptionHandler(SigUsr1, reinterpret_cast(&Handler)) == 0); Pthread writer = nullptr; Require(scePthreadCreate(&writer, nullptr, Writer, nullptr, "pipe writer") == 0); - while (blocks.load() == 0) std::this_thread::yield(); for (int raise = 1; raise <= Raises; ++raise) { + while (blocked.load() < raise) std::this_thread::yield(); + if (mode == "--delay-raise") std::this_thread::sleep_for(std::chrono::milliseconds(200)); Require(sceKernelRaiseException(writer, SigUsr1) == 0); while (deliveries.load() < raise) std::this_thread::yield(); Require(std::fputc('y', shared) == 'y'); released.store(raise); } - stop.store(true); - draining.store(false); Require(scePthreadJoin(writer, nullptr) == 0); Require(sceKernelRemoveExceptionHandler(SigUsr1) == 0); std::fclose(shared); reader.join(); CloseHandle(read); + CloseHandle(writerThread.load()); done.store(true); watchdog.join(); } diff --git a/core/libs/tests/GuestRandomState.cpp b/core/libs/tests/GuestRandomState.cpp index a660db24f8..6e126f762d 100644 --- a/core/libs/tests/GuestRandomState.cpp +++ b/core/libs/tests/GuestRandomState.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include #include @@ -40,10 +41,10 @@ static bool SeedIs(const std::uint16_t* seed, std::uint16_t low, std::uint16_t m static void CheckRand48() { std::uint16_t seed[3] = {0x330e, 0xabcd, 0x1234}; - Require(erand48_nid_postfix(seed) == 0x657eb7255101p-48 && SeedIs(seed, 0x5101, 0xb725, 0x657e), "erand48 first value"); + Require(erand48_nid_postfix(seed) == 0x657eb725p-32 && SeedIs(seed, 0x5101, 0xb725, 0x657e), "erand48 first value"); Require(nrand48_nid_postfix(seed) == 1804928587 && SeedIs(seed, 0x6378, 0x0c96, 0xd72a), "nrand48 second value"); Require(jrand48_nid_postfix(seed) == 1517566982 && SeedIs(seed, 0x2a23, 0x3c06, 0x5a74), "jrand48 third value"); - Require(erand48_nid_postfix(seed) == 0x72534abf62f2p-48 && SeedIs(seed, 0x62f2, 0x4abf, 0x7253), "erand48 fourth value"); + Require(erand48_nid_postfix(seed) == 0x72534abfp-32 && SeedIs(seed, 0x62f2, 0x4abf, 0x7253), "erand48 fourth value"); Require(nrand48_nid_postfix(seed) == 684387517 && SeedIs(seed, 0x8d15, 0xd97a, 0x5195), "nrand48 fifth value"); Require(jrand48_nid_postfix(seed) == 3807181130 && SeedIs(seed, 0xeffc, 0xf94a, 0xe2ec), "jrand48 keeps bit 31 without sign extension"); @@ -89,12 +90,15 @@ static void CheckRand48() { srand48_nid_postfix(1); for (const std::int64_t expected : consoleMrand48) Require(mrand48_nid_postfix() == expected, "mrand48 matches the console after srand48(1)"); + constexpr std::array consoleDrand48 {0x4435afd7p-32, 0xae99dc60p-32, 0x43f73108p-32}; + srand48_nid_postfix(7); + for (const double expected : consoleDrand48) Require(drand48_nid_postfix() == expected, "drand48 matches the titles' libc.prx after srand48(7)"); #ifndef _WIN32 std::uint16_t guest[3] = {0x1234, 0x5678, 0x9abc}; unsigned short host[3] = {0x1234, 0x5678, 0x9abc}; for (int round = 0; round < 64; ++round) { - Require(erand48_nid_postfix(guest) == ::erand48(host), "erand48 matches the host"); + Require(erand48_nid_postfix(guest) == std::ldexp(std::floor(::erand48(host) * 0x1p32), -32), "erand48 keeps the host's upper 32 bits"); Require(nrand48_nid_postfix(guest) == ::nrand48(host), "nrand48 matches the host"); Require(jrand48_nid_postfix(guest) == static_cast(::jrand48(host)), "jrand48 matches the host's 32 bits"); } @@ -102,7 +106,7 @@ static void CheckRand48() { srand48_nid_postfix(1234567); for (int round = 0; round < 32; ++round) { - Require(drand48_nid_postfix() == ::drand48(), "drand48 matches the host"); + Require(drand48_nid_postfix() == std::ldexp(std::floor(::drand48() * 0x1p32), -32), "drand48 keeps the host's upper 32 bits"); Require(lrand48_nid_postfix() == ::lrand48(), "lrand48 matches the host"); Require(mrand48_nid_postfix() == static_cast(::mrand48()), "mrand48 matches the host's 32 bits"); } diff --git a/core/libs/tests/GuestRegMgr.cpp b/core/libs/tests/GuestRegMgr.cpp index f9e8901ef4..ec355a78f7 100644 --- a/core/libs/tests/GuestRegMgr.cpp +++ b/core/libs/tests/GuestRegMgr.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include @@ -21,4 +22,26 @@ int main() { std::int32_t value = 0; Require(ReportsNotImplemented("sceRegMgrNonSysGetInt", [&] { sceRegMgrNonSysGetInt(0, 0, &value); })); Require(ReportsNotImplemented("sceRegMgrNonSysGetInt", [&] { sceRegMgrNonSysGetInt(-1, 1, nullptr); })); + + int32_t intVal = 0; + char strBuf[64] = {}; + uint8_t binBuf[32] = {}; + + Require(sceRegMgrGetInt(0x1000, nullptr) == static_cast(0x80820001)); + Require(sceRegMgrGetStr(0x1001, nullptr, 10) == static_cast(0x80820001)); + Require(sceRegMgrGetStr(0x1001, strBuf, -1) == static_cast(0x80820001)); + Require(sceRegMgrGetBin(0x1002, nullptr, 10) == static_cast(0x80820001)); + Require(sceRegMgrGetBin(0x1002, binBuf, -1) == static_cast(0x80820001)); + + intVal = 0x55AA; + Require(sceRegMgrGetInt(888888, &intVal) == static_cast(0x80820002)); + Require(intVal == 0x55AA); + std::strcpy(strBuf, "CANARY"); + Require(sceRegMgrGetStr(888888, strBuf, sizeof(strBuf)) == static_cast(0x80820002)); + Require(std::strcmp(strBuf, "CANARY") == 0); + binBuf[0] = 0x77; + Require(sceRegMgrGetBin(888888, binBuf, sizeof(binBuf)) == static_cast(0x80820002)); + Require(binBuf[0] == 0x77); + + return 0; } diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 10a2900e2f..92173f92ba 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -16,6 +15,7 @@ #include #include #include +#include static void Require(bool value) { if (!value) std::abort(); } @@ -64,7 +64,7 @@ static void SetCompiledSdkVersion(std::byte* process, std::uint32_t version) { } static void EnterTitleDirectory(const char* titleId) { - const auto directory = std::filesystem::temp_directory_path() / ("rtc-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + const auto directory = std::filesystem::temp_directory_path() / ("rtc-test-" + std::to_string(std::random_device{}())); std::filesystem::create_directories(directory / "app0" / "sce_sys"); std::filesystem::current_path(directory); if (titleId == nullptr) return; @@ -186,6 +186,25 @@ int main(int argc, char** argv) { Require(sceRtcGetDosTime(&dosEarly, &dosTime) == invalidYear && dosTime == 0); const RtcDateTime dosLate{2108, 1, 1, 0, 0, 0, 0}; Require(sceRtcGetDosTime(&dosLate, &dosTime) == invalidYear && dosTime == 0xff9fbf7du); + const RtcDateTime dosMeasured{2026, 10, 22, 21, 30, 59, 999999}; + Require(sceRtcGetDosTime(&dosMeasured, &dosTime) == 0 && dosTime == 0x5d56abddu); + const RtcDateTime dosFirstYear{1, 1, 1, 0, 0, 0, 0}; + Require(sceRtcGetDosTime(&dosFirstYear, &dosTime) == invalidYear && dosTime == 0); + const RtcDateTime dosLastYear{9999, 12, 31, 23, 59, 59, 0}; + Require(sceRtcGetDosTime(&dosLastYear, &dosTime) == invalidYear && dosTime == 0xff9fbf7du); + dosTime = 0xbbbbbbbbu; + const RtcDateTime dosYearZero{0, 1, 1, 0, 0, 0, 0}; + Require(sceRtcGetDosTime(&dosYearZero, &dosTime) == invalidYear && dosTime == 0xbbbbbbbbu); + const RtcDateTime dosBadDay{2026, 2, 30, 0, 0, 0, 0}; + Require(sceRtcGetDosTime(&dosBadDay, &dosTime) == invalidDay && dosTime == 0xbbbbbbbbu); + const RtcDateTime dosBadHour{2026, 1, 1, 24, 0, 0, 0}; + Require(sceRtcGetDosTime(&dosBadHour, &dosTime) == invalidHour && dosTime == 0xbbbbbbbbu); + const RtcDateTime dosBadMicrosecond{2026, 1, 1, 0, 0, 0, 1000000}; + Require(sceRtcGetDosTime(&dosBadMicrosecond, &dosTime) == invalidMicrosecond && dosTime == 0xbbbbbbbbu); + RtcDateTime dosDecoded{}; + Require(sceRtcSetDosTime(&dosDecoded, 0xffffffffu) == 0 && Equal(dosDecoded, RtcDateTime{2107, 15, 31, 31, 63, 62, 0})); + Require(sceRtcSetDosTime(&dosDecoded, 0) == 0 && Equal(dosDecoded, RtcDateTime{1980, 0, 0, 0, 0, 0, 0})); + Require(sceRtcSetDosTime(nullptr, 0) == invalidPointer); Require(sceRtcGetYear(nullptr) == invalidPointer); Require(sceRtcSetYear(nullptr, 2024) == invalidPointer); @@ -762,10 +781,11 @@ int main(int argc, char** argv) { RtcTick now{}; Require(sceRtcGetCurrentTick(&now) == 0 && now.tick > leapDayTick); + const RtcTick fixed{leapDayTick}; RtcTick local{}; RtcTick back{}; - Require(sceRtcConvertUtcToLocalTime(&now, &local) == 0); - Require(sceRtcConvertLocalTimeToUtc(&local, &back) == 0 && back.tick == now.tick); + Require(sceRtcConvertUtcToLocalTime(&fixed, &local) == 0); + Require(sceRtcConvertLocalTimeToUtc(&local, &back) == 0 && back.tick == fixed.tick); char rfc2822[32]; tick.tick = leapDayTick; diff --git a/core/libs/tests/GuestRuneLocale.cpp b/core/libs/tests/GuestRuneLocale.cpp index b05986b76d..31a506c9f8 100644 --- a/core/libs/tests/GuestRuneLocale.cpp +++ b/core/libs/tests/GuestRuneLocale.cpp @@ -12,7 +12,6 @@ extern "C" { extern const unsigned char _DefaultRuneLocale_nid_postfix[4224]; extern const unsigned char* _CurrentRuneLocale_nid_postfix; extern int __mb_sb_limit_nid_postfix; - } void Require(bool condition, std::source_location location = std::source_location::current()) { @@ -85,6 +84,10 @@ int main() { } Require(entry.wide(0xffffffff) == 0 && iswctype_nid_postfix(0xffffffff, entry.mask) == 0); } + for (std::uint32_t c = 0; c < 300; ++c) { + Require((iswascii_nid_postfix(c) != 0) == (c <= 0x7f)); + } + Require(iswascii_nid_postfix(0xffffffff) == 0); Require(wctype_nid_postfix("ideogram") == 0x80000 && wctype_nid_postfix("special") == 0x100000); Require(wctype_nid_postfix("phonogram") == 0x200000 && wctype_nid_postfix("number") == 0x400000); Require(wctype_nid_postfix("rune") == 0xffffff00 && wctype_nid_postfix("") == 0 && wctype_nid_postfix("Alpha") == 0); diff --git a/core/libs/tests/GuestSafeErrors.cpp b/core/libs/tests/GuestSafeErrors.cpp index b253f14d6c..ee6ad4bd74 100644 --- a/core/libs/tests/GuestSafeErrors.cpp +++ b/core/libs/tests/GuestSafeErrors.cpp @@ -46,7 +46,7 @@ void CheckMessage(int error, const char* expected) { wanted.resize(capacity - 1); if (capacity > 3) wanted.replace(capacity - 4, 3, "..."); } - Require(result == (capacity <= full.size() ? 34 : 0), "complete versus truncated return"); + Require(result == 0, "complete and truncated messages both return 0"); Require(std::strcmp(buffer.data() + 1, wanted.c_str()) == 0, "truncated text, ellipsis and terminator"); Require(buffer[0] == '#', "leading canary"); for (std::size_t index = wanted.size() + 2; index < buffer.size(); ++index) @@ -56,15 +56,17 @@ void CheckMessage(int error, const char* expected) { } void CheckMessages() { - CheckMessage(0, "No error: 0"); + CheckMessage(0, "No error"); CheckMessage(2, "No such file or directory"); CheckMessage(11, "Resource deadlock avoided"); CheckMessage(35, "Resource temporarily unavailable"); - CheckMessage(96, "Previous owner died"); - CheckMessage(-1, "Unknown error: -1"); - CheckMessage(97, "Unknown error: 97"); - CheckMessage(std::numeric_limits::min(), "Unknown error: -2147483648"); - CheckMessage(std::numeric_limits::max(), "Unknown error: 2147483647"); + CheckMessage(22, "Invalid argument"); + CheckMessage(95, "error_code=0x0000005f"); + CheckMessage(96, "error_code=0x00000060"); + CheckMessage(-1, "error_code=0xffffffff"); + CheckMessage(97, "error_code=0x00000061"); + CheckMessage(std::numeric_limits::min(), "error_code=0x80000000"); + CheckMessage(std::numeric_limits::max(), "error_code=0x7fffffff"); for (int error = 0; error < 97; ++error) { char expected[128]; Require(strerror_r_nid_postfix(error, expected, sizeof(expected)) == 0, "existing guest error table"); @@ -104,7 +106,7 @@ void CheckThreads() { Require(violations == 100, "callbacks execute in the calling thread"); }; std::thread first(worker, 35, "Resource temporarily unavailable"); - std::thread second(worker, -1, "Unknown error: -1"); + std::thread second(worker, -1, "error_code=0xffffffff"); first.join(); second.join(); } diff --git a/core/libs/tests/GuestSaveDataConvert.cpp b/core/libs/tests/GuestSaveDataConvert.cpp index 85fd30503b..13085f00db 100644 --- a/core/libs/tests/GuestSaveDataConvert.cpp +++ b/core/libs/tests/GuestSaveDataConvert.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -26,6 +27,15 @@ void Check(bool condition, const std::string& what) { ++failures; } +bool Throws(const SaveDataConvert* convert) { + try { + sceSaveDataConvert(convert); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + int Convert(const char* data, std::size_t size) { SceSaveDataDirName name{}; std::memcpy(name.data, data, size); @@ -53,6 +63,9 @@ int main() { float progress = 0.0f; Check(sceSaveDataGetConvertProgress(&progress) == SaveDataErrorNotInitialized, "query before initialize fails with not initialized"); + Check(Throws(nullptr), "null convert pointer throws"); + SaveDataConvert nullDir{}; + Check(Throws(&nullDir), "null dir_name throws"); Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes"); Check(sceSaveDataGetConvertProgress(&progress) == 0, "get convert progress succeeds"); Check(progress == 0.0f, "convert progress reports 0.0f initially"); diff --git a/core/libs/tests/GuestSaveDataDelete.cpp b/core/libs/tests/GuestSaveDataDelete.cpp index b84152d310..c8f243059d 100644 --- a/core/libs/tests/GuestSaveDataDelete.cpp +++ b/core/libs/tests/GuestSaveDataDelete.cpp @@ -81,6 +81,9 @@ int main() { char unterminated[sizeof(SceSaveDataDirName::data)]; std::memset(unterminated, 'a', sizeof(unterminated)); Check(Delete(unterminated, sizeof(unterminated)) == SaveDataErrorParameter, "an unterminated name is rejected"); + Check(sceSaveDataDelete(nullptr) == SaveDataErrorParameter, "null del returns PARAMETER"); + SaveDataDelete nullDirDel{}; + Check(sceSaveDataDelete(&nullDirDel) == SaveDataErrorParameter, "null dir_name returns PARAMETER"); constexpr char mountedPayload[] = "live mounted save"; constexpr char updatedPayload[] = "still mounted"; diff --git a/core/libs/tests/GuestSaveDataMountStatus.cpp b/core/libs/tests/GuestSaveDataMountStatus.cpp index 6f9f2ed126..596fc94026 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -9,6 +9,9 @@ #include #include #include +#ifdef _WIN32 +#include +#endif namespace { @@ -35,6 +38,11 @@ void CheckUnmountedCalls() { SaveDataIcon icon{}; char buf[128]{}; std::size_t gotSize = 0; + SaveDataMount3 nullMount{}; + SaveDataMountResult mountResult{}; + Check(sceSaveDataMount3(nullptr, &mountResult) == SaveDataErrorParameter, "mount3 null mount is PARAMETER"); + Check(sceSaveDataMount3(&nullMount, nullptr) == SaveDataErrorParameter, "mount3 null result is PARAMETER"); + Check(sceSaveDataMount3(&nullMount, &mountResult) == SaveDataErrorParameter, "mount3 null dir_name is PARAMETER"); Check(sceSaveDataUmount2(0, nullptr) == SaveDataErrorParameter, "umount2 null point is PARAMETER"); Check(sceSaveDataUmount2(0, &point) == SaveDataErrorNotMounted, "umount2 unmounted point is NOT_MOUNTED"); Check(sceSaveDataLoadIcon(nullptr, &icon) == SaveDataErrorParameter, "loadIcon null point is PARAMETER"); @@ -151,7 +159,15 @@ void CheckEquivalentMounts() { const auto casePath = std::filesystem::path("_sd") / "IDENTITY"; const bool caseAlias = std::filesystem::exists(casePath) && std::filesystem::equivalent(originalPath, casePath, error); Check(!error || error == std::errc::no_such_file_or_directory, "case-alias identity check succeeds"); +#ifdef _WIN32 + error.clear(); + if (!CreateSymbolicLinkW((std::filesystem::path("_sd") / "linked").c_str(), L"identity", + SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE)) { + error = std::error_code(GetLastError(), std::system_category()); + } +#else std::filesystem::create_directory_symlink("identity", std::filesystem::path("_sd") / "linked", error); +#endif const bool symlinkAlias = !error; Check(caseAlias || symlinkAlias, "a host alias is available to check mount identity"); @@ -173,7 +189,12 @@ void CheckEquivalentMounts() { if (caseAlias) MountAndCheck("IDENTITY", MountReadWrite, 0u, "case alias after unmount"); if (symlinkAlias) { MountAndCheck("linked", MountReadWrite, 0u, "symlink alias after unmount"); +#ifdef _WIN32 + error.clear(); + if (!RemoveDirectoryW((std::filesystem::path("_sd") / "linked").c_str())) error = std::error_code(GetLastError(), std::system_category()); +#else std::filesystem::remove(std::filesystem::path("_sd") / "linked", error); +#endif Check(!error, "symlink alias is removed"); } } @@ -220,6 +241,7 @@ int main() { const auto previous = std::filesystem::current_path(); std::filesystem::current_path(root); CheckUnmountedCalls(); + Check(sceSaveDataSyncSaveDataMemory(nullptr) == static_cast(0x809F0000), "null param rejected in memory sync"); Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes"); MountAndCheck("fresh", MountReadWrite | MountCreate2, 1u, "CREATE2 of a missing save"); MountAndCheck("fresh", MountReadWrite | MountCreate2, 0u, "CREATE2 of an existing save"); diff --git a/core/libs/tests/GuestSaveDataPs4.cpp b/core/libs/tests/GuestSaveDataPs4.cpp index d707094961..0c4a45540f 100644 --- a/core/libs/tests/GuestSaveDataPs4.cpp +++ b/core/libs/tests/GuestSaveDataPs4.cpp @@ -107,7 +107,7 @@ void TestMountPs4() { std::strcpy(dir.data, "kept"); mount.dir_name = &dir; mount.mount_mode = MountReadWrite; - Check(ThrowsRuntimeError([&] { sceSaveDataMount(&mount, nullptr); }), "null mount_result throws"); + Check(sceSaveDataMount(&mount, nullptr) == SaveDataErrorParameter, "null mount_result is PARAMETER"); Check(sceSaveDataMount(&mount, &result) == 0, "mount existing save directory succeeds"); Check(result.mount_point.data[0] != '\0', "mount writes mount point"); Check(sceSaveDataUmount(&result.mount_point) == 0, "umount mounted point succeeds"); diff --git a/core/libs/tests/GuestSaveDataSearch.cpp b/core/libs/tests/GuestSaveDataSearch.cpp index f3b0b4382a..ede79c6c08 100644 --- a/core/libs/tests/GuestSaveDataSearch.cpp +++ b/core/libs/tests/GuestSaveDataSearch.cpp @@ -19,15 +19,6 @@ static void Require(bool value, const char* message) { if (!value) throw std::runtime_error(message); } -template -static bool ThrowsRuntimeError(TFunction function) { - try { - function(); - } catch (const std::runtime_error&) { - return true; - } - return false; -} static std::vector Search(std::uint32_t key, std::uint32_t order, std::uint32_t capacity, std::uint32_t* hits) { SceSaveDataDirName pattern{}; @@ -283,12 +274,16 @@ static void CheckNullArguments() { SaveDataDirNameSearchCond cond{}; cond.user_id = 1; SaveDataDirNameSearchResult result{}; - Require(ThrowsRuntimeError([&] { sceSaveDataDirNameSearch(nullptr, &result); }), "null cond throws in sceSaveDataDirNameSearch"); - Require(ThrowsRuntimeError([&] { sceSaveDataDirNameSearch(&cond, nullptr); }), "null result throws in sceSaveDataDirNameSearch"); - Require(ThrowsRuntimeError([&] { sceSaveDataDirNameSearch2(nullptr, &result); }), "null cond throws in sceSaveDataDirNameSearch2"); - Require(ThrowsRuntimeError([&] { sceSaveDataDirNameSearch2(&cond, nullptr); }), "null result throws in sceSaveDataDirNameSearch2"); + Require(sceSaveDataDirNameSearch(nullptr, &result) == SaveDataErrorParameter, "null cond returns PARAMETER in sceSaveDataDirNameSearch"); + Require(sceSaveDataDirNameSearch(&cond, nullptr) == SaveDataErrorParameter, "null result returns PARAMETER in sceSaveDataDirNameSearch"); + Require(sceSaveDataDirNameSearch2(nullptr, &result) == SaveDataErrorParameter, "null cond returns PARAMETER in sceSaveDataDirNameSearch2"); + Require(sceSaveDataDirNameSearch2(&cond, nullptr) == SaveDataErrorParameter, "null result returns PARAMETER in sceSaveDataDirNameSearch2"); cond.order = 2; Require(sceSaveDataDirNameSearch2(&cond, &result) == SaveDataErrorParameter, "invalid order must be rejected in sceSaveDataDirNameSearch2"); + Require(sceSaveDataGetConvertProgress(nullptr) == SaveDataErrorParameter, "null progress returns PARAMETER in sceSaveDataGetConvertProgress"); + float progress = 0.0f; + Require(sceSaveDataGetConvertProgress(&progress) == -2137063423, "uninitialized convert progress returns NOT_INITIALIZED"); + Require(sceSaveDataCheckIpmiIfSize() == 0, "sceSaveDataCheckIpmiIfSize succeeds"); } static void BlockSize() { diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index b973785b8c..77e3e7ad0d 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -86,6 +86,34 @@ static bool Failed(std::int64_t result, int error) { return result == static_cast(0x80410100u | static_cast(error)) && *sceNetErrnoLoc() == error; } +template +static std::string CaptureStdout(TBody body) { + std::fflush(stdout); + std::FILE* capture = std::tmpfile(); + Require(capture != nullptr); +#ifdef _WIN32 + const int saved = _dup(_fileno(stdout)); + Require(saved >= 0 && _dup2(_fileno(capture), _fileno(stdout)) == 0); +#else + const int saved = dup(fileno(stdout)); + Require(saved >= 0 && dup2(fileno(capture), fileno(stdout)) >= 0); +#endif + body(); + std::fflush(stdout); +#ifdef _WIN32 + Require(_dup2(saved, _fileno(stdout)) == 0); + _close(saved); +#else + Require(dup2(saved, fileno(stdout)) >= 0); + close(saved); +#endif + std::string text; + std::rewind(capture); + for (int character = std::fgetc(capture); character != EOF; character = std::fgetc(capture)) text.push_back(static_cast(character)); + std::fclose(capture); + return text; +} + static void CheckSocketHostErrors() { Require(Failed(sceNetSocket(nullptr, 2, 1, 17), 43)); } @@ -477,6 +505,40 @@ static void CheckEmptyEpoll() { } } +static void CheckRejectedArguments() { + Require(Failed(sceNetEpollCreate(nullptr, 0), 22)); + for (const int flags : {1, 0x10000, -1}) Require(Failed(sceNetEpollCreate("rejected-epoll", flags), 22)); + + std::array untouched{}; + untouched.fill(0xA5); + NetEtherAddr mac{}; + std::memcpy(mac.data, untouched.data(), untouched.size()); + for (const int flags : {1, 0x10000, -1}) { + Require(Failed(sceNetGetMacAddress(&mac, flags), 22)); + Require(std::memcmp(mac.data, untouched.data(), untouched.size()) == 0); + } + Require(Failed(sceNetGetMacAddress(nullptr, 1), 22)); + + for (const int flags : {1, 0x10000, -1}) Require(Failed(sceNetResolverCreate("rejected-resolver", 0, flags), 22)); + + std::array ipv4{}; + const int resolver = sceNetResolverCreate("rejected-ntoa", 0, 0); + Require(resolver >= 0); + Require(sceNetResolverAbort(resolver, 1) == 0); + Require(Failed(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), -1, 1, 0), 22)); + Require(Failed(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, -1, 0), 22)); + for (const int flags : {2, 0x20000, 0x10003, static_cast(0x80000000u), -1}) + Require(Failed(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, flags), 22)); + Require(Failed(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0), 4)); + Require(sceNetResolverDestroy(resolver) == 0); + + Require(Failed(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), -1, 0, 0), 22)); + Require(Failed(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 0, -1, 0), 22)); + Require(Failed(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 0, 0, 2), 22)); + for (const int flags : {0, 1, 0x10000, 0x10001}) + Require(Failed(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 0, 0, flags), 9)); +} + static void CheckDontWait() { constexpr int DontWait = 0x80; constexpr int Peek = 0x2; @@ -1041,14 +1103,17 @@ static void CheckSystemTime() { } static void CheckUsleep() { - Require(sceNetUsleep(0) == 0); + Require(CaptureStdout([] { Require(sceNetUsleep(0) == 0); }).empty()); const auto start = std::chrono::steady_clock::now(); Require(sceNetUsleep(2000) == 0); Require(std::chrono::steady_clock::now() - start >= std::chrono::microseconds(2000)); - *sceNetErrnoLoc() = 0; - Require(Failed(sceNetUsleep(-1), 22)); - *sceNetErrnoLoc() = 0; - Require(Failed(sceNetUsleep(INT32_MIN), 22)); + const std::string printed = CaptureStdout([] { + *sceNetErrnoLoc() = 0; + Require(Failed(sceNetUsleep(-1), 22)); + *sceNetErrnoLoc() = 0; + Require(Failed(sceNetUsleep(INT32_MIN), 22)); + }); + Require(printed == "[LIBNET] usleep -1\n[LIBNET] usleep -2147483648\n"); } static void CheckEpollIdent() { @@ -1077,6 +1142,39 @@ static void CheckEpollIdent() { Require(sceNetSocketClose(sender) == 0 && sceNetSocketClose(receiver) == 0); } +static void CheckEpollClosedStream() { + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + const int listener = sceNetSocket("closed-stream-listener", 2, 1, 6); + Require(listener >= 0); + Require(sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0 && sceNetListen(listener, 1) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0); + const int client = sceNetSocket("closed-stream-client", 2, 1, 6); + Require(client >= 0 && sceNetConnect(client, address.data(), address.size()) == 0); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0); + const int epoll = sceNetEpollCreate("epoll-closed-stream", 0); + Require(epoll >= 0); + NetEpollEvent registration{}; + registration.events = 1; + registration.ident = static_cast(accepted); + Require(sceNetEpollControl(epoll, 1, accepted, ®istration) == 0); + Require(sceNetSocketClose(client) == 0); + NetEpollEvent ready{}; + Require(sceNetEpollWait(epoll, &ready, 1, 1000000) == 1 && ready.events == 1); + char byte = 0; + Require(sceNetRecv(accepted, &byte, 1, 0) == 0); + registration.events = 3; + Require(sceNetEpollControl(epoll, 2, accepted, ®istration) == 0); + Require(sceNetEpollWait(epoll, &ready, 1, 0) == 1 && ready.events == 3); + Require(sceNetShutdown(accepted, 1) == 0); + registration.events = 1; + Require(sceNetEpollControl(epoll, 2, accepted, ®istration) == 0); + Require(sceNetEpollWait(epoll, &ready, 1, 0) == 1 && ready.events == (1 | 16)); + Require(sceNetEpollDestroy(epoll) == 0); + Require(sceNetSocketClose(accepted) == 0 && sceNetSocketClose(listener) == 0); +} + static void CheckTcpNoDelay() { const int stream = sceNetSocket("tcp-nodelay", 2, 1, 6); const int datagram = sceNetSocket("tcp-nodelay-udp", 2, 2, 17); @@ -1129,6 +1227,13 @@ static bool HostIpv6LoopbackUnavailable() { #endif } +static void CheckRejectedAddressText(const char* text) { + std::array address; + address.fill(0xa5); + Require(sceNetInetPton(28, text, address.data()) == 0); + Require(address[0] == 0xa5 && address[15] == 0xa5); +} + static void CheckLinkLocalAllNodesIpv6() { Require(reinterpret_cast(sce_net_in6addr_linklocal_allnodes) % alignof(std::uint32_t) == 0); std::array address{}; @@ -1793,10 +1898,16 @@ int main() { CheckNetMemory(); CheckNetstat(); CheckEmptyEpoll(); + CheckRejectedArguments(); CheckDontWait(); CheckAddressText(2, "127.0.0.1"); CheckAddressText(2, "255.255.255.255"); CheckAddressText(28, "::1"); + CheckAddressText(28, "::ffff:0.0.0.1"); + CheckAddressText(28, "::ffff:0:102:304"); + CheckAddressText(28, "fe80::5efe:102:304"); + CheckRejectedAddressText("::01.2.3.4"); + CheckRejectedAddressText("::ffff:01.2.3.4"); CheckAddressText(28, "1234:5678:9abc:def0:1234:5678:9abc:def0"); CheckInetPtonWhitespace(); CheckInetPtonExParts(); @@ -1804,6 +1915,7 @@ int main() { CheckUsleep(); CheckSystemTime(); CheckEpollIdent(); + CheckEpollClosedStream(); CheckTcpNoDelay(); CheckLinkLocalAllNodesIpv6(); CheckUnboundSocketName(); @@ -2252,13 +2364,11 @@ int main() { Require(std::strcmp(text, reinterpret_cast(info.opaque)) == 0); } for (const int flags : {0x1, -1}) { - bool threw = false; - try { sceNetGetDnsInfo(reinterpret_cast(dns.data()), flags); } catch (const std::runtime_error&) { threw = true; } - Require(threw); + const auto before = dns; + Require(Failed(sceNetGetDnsInfo(reinterpret_cast(dns.data()), flags), 22)); + Require(dns == before); } - bool threw = false; - try { sceNetGetDnsInfo(nullptr, 0); } catch (const std::runtime_error&) { threw = true; } - Require(threw); + Require(Failed(sceNetGetDnsInfo(nullptr, 0), 22)); } NetCtlIfStat first{}; first.device = 7; diff --git a/core/libs/tests/GuestSceV2Exceptions.cpp b/core/libs/tests/GuestSceV2Exceptions.cpp index 60765c0a45..6683c8d67b 100644 --- a/core/libs/tests/GuestSceV2Exceptions.cpp +++ b/core/libs/tests/GuestSceV2Exceptions.cpp @@ -111,7 +111,9 @@ static void CheckThrowers() { {"_Throw_Cpp_error", AsThrower(_ZNSt8__sce_v216_Throw_Cpp_errorEi_nid_postfix), reinterpret_cast(1), &systemError, &_ZTISt12system_error_nid_postfix, 2, "NSt8__sce_v212system_errorE|invalid argument: invalid argument"}, }; + inspectBytes = sizeof(void*); for (const auto& test : cases) Run(test); + inspectBytes = sizeof(LibcException::ExceptionObject); } static void CheckCopies() { diff --git a/core/libs/tests/GuestSelect.cpp b/core/libs/tests/GuestSelect.cpp index 55c7be7b9a..1c1f48d9dd 100644 --- a/core/libs/tests/GuestSelect.cpp +++ b/core/libs/tests/GuestSelect.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -55,6 +56,30 @@ static KernelEvent UserEvent(std::uint16_t flags, std::uint32_t fflags) { return change; } +static void StandardDescriptorFile(int descriptor) { + const int saved = dup_nid_postfix(descriptor); + const std::string path = "anyps5-select-input-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const int file = open_nid_postfix(path.c_str(), 0x0002 | 0x0200, 0644); + Require(file >= 0); + Require(dup2_nid_postfix(file, descriptor) == descriptor); + if (file != descriptor) Require(close_nid_postfix(file) == 0); + GuestDescriptorSet read{}; + GuestDescriptorSet write{}; + GuestDescriptorSet except{}; + const GuestTimeval zero{0, 0}; + const int ready = SelectAll(descriptor, read, write, except, zero); + if (ready != 2) std::fprintf(stderr, "select on a regular file at descriptor %d returned %d instead of 2\n", descriptor, ready); + Require(ready == 2); + Require(read.Has(descriptor) && write.Has(descriptor) && !except.Has(descriptor)); + char byte = 0; + Require(read_nid_postfix(descriptor, &byte, 1) == 0); + if (saved >= 0) { + Require(dup2_nid_postfix(saved, descriptor) == descriptor); + Require(close_nid_postfix(saved) == 0); + } else Require(close_nid_postfix(descriptor) == 0); + Require(unlink_nid_postfix(path.c_str()) == 0); +} + static void Kqueues() { const int kq = kqueue_nid_postfix(); Require(kq >= 0 && kq < GuestDescriptorLimit); @@ -85,6 +110,7 @@ static void Kqueues() { } int main() { + for (const int descriptor : {0, 1, 2}) StandardDescriptorFile(descriptor); Kqueues(); int descriptors[2]{-1, -1}; @@ -185,14 +211,43 @@ int main() { Require(read.Has(reader) && write.Has(writer)); } + Require(reader < 63); + + { + GuestDescriptorSet read{}; + GuestDescriptorSet except{}; + read.Add(reader); + read.Add(63); + read.Add(64); + except.Add(63); + GuestTimeval limit{0, 100000}; + Require(select_nid_postfix(reader, read.words.data(), nullptr, except.words.data(), &limit) == 0); + Require(!read.Has(reader) && !read.Has(63) && read.Has(64) && !except.Has(63)); + } + { GuestDescriptorSet read{}; read.Add(reader); - GuestTimeval limit{0, 100000}; - Require(select_nid_postfix(reader, read.words.data(), nullptr, nullptr, &limit) == 0); + const GuestTimeval zero{0, 0}; + Require(select_nid_postfix(0, read.words.data(), nullptr, nullptr, &zero) == 0); Require(read.Has(reader)); } + { + const char byte = 'n'; + Require(write_nid_postfix(writer, &byte, 1) == 1); + GuestDescriptorSet read{}; + read.Add(reader); + read.Add(63); + read.Add(64); + GuestTimeval limit{0, 200000}; + Require(select_nid_postfix(reader + 1, read.words.data(), nullptr, nullptr, &limit) == 1); + Require(read.Has(reader) && !read.Has(63) && read.Has(64)); + char buffer[8]{}; + const auto count = read_nid_postfix(reader, buffer, sizeof(buffer)); + Require(count >= 1 && buffer[count - 1] == byte); + } + { GuestDescriptorSet read{}; read.Add(reader); diff --git a/core/libs/tests/GuestSelectWait.cpp b/core/libs/tests/GuestSelectWait.cpp index 3c9044117d..e5dd072cd4 100644 --- a/core/libs/tests/GuestSelectWait.cpp +++ b/core/libs/tests/GuestSelectWait.cpp @@ -53,14 +53,17 @@ int main() { } std::array read{}; read[0] = 1 | (std::uint64_t{1} << 63); + read[1] = 1; const GuestTimeval limit{0, 500}; const auto start = Clock::now(); Require(select_nid_postfix(1, read.data(), nullptr, nullptr, &limit) == 0, "select with no ready device did not time out"); Require(Clock::now() - start >= std::chrono::microseconds(limit.microseconds), "select with no ready device returned before its deadline"); - Require(read[0] == (std::uint64_t{1} << 63), - "select did not preserve bits outside the descriptor limit"); + Require(read[0] == 0, + "select did not clear the bits up to the end of the last copied word"); + Require(read[1] == 1, + "select did not preserve the words past the descriptor limit"); std::array write{}; write[0] = 2; const GuestTimeval longLimit{1, 0}; diff --git a/core/libs/tests/GuestSendfile.cpp b/core/libs/tests/GuestSendfile.cpp index 388a426d80..f2fac5d3cc 100644 --- a/core/libs/tests/GuestSendfile.cpp +++ b/core/libs/tests/GuestSendfile.cpp @@ -80,6 +80,17 @@ int main() { Require(unconnected >= 0); sent = 77; Require(Fails(sendfile_nid_postfix(file, unconnected, size + 10, 0, nullptr, &sent, 0), 57) && sent == 0); + const int random = open_nid_postfix("/dev/urandom", 0, 0); + Require(random >= 0); + sent = 77; + Require(Fails(sendfile_nid_postfix(random, pair[0], 0, 16, &parts, &sent, 0), 22) && sent == 0); + sent = 77; + Require(Fails(sendfile_nid_postfix(random, pair[0], 0, 0, nullptr, &sent, 0), 22) && sent == 0); + const int queue = kqueue_nid_postfix(); + Require(queue >= 0); + sent = 77; + Require(Fails(sendfile_nid_postfix(queue, pair[0], 0, 0, nullptr, &sent, 0), 22) && sent == 0); + Require(close_nid_postfix(queue) == 0 && close_nid_postfix(random) == 0); const std::string large = Content(32 << 20); const int largeFile = OpenWritten("sendfile_large.bin", large); diff --git a/core/libs/tests/GuestSharePlay.cpp b/core/libs/tests/GuestSharePlay.cpp index 1bb330a5a8..49596cd69a 100644 --- a/core/libs/tests/GuestSharePlay.cpp +++ b/core/libs/tests/GuestSharePlay.cpp @@ -1,4 +1,4 @@ -#include "prx/libSceSharePlay/include/Api.hpp" +#include "prx/libSceSharePlay/libSceSharePlay.hpp" #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include @@ -26,6 +26,9 @@ int main() { constexpr int invalidArgument = static_cast(0x810E0001); Require(sceSharePlayTerminate() == notInitialized); Require(sceSharePlayGetCurrentConnectionInfoA(nullptr) == notInitialized); + Require(sceSharePlayGetCurrentConnectionInfo(nullptr) == notInitialized); + Require(sceSharePlayProhibitController(0) == notInitialized); + Require(sceSharePlayProhibitController(1) == notInitialized); Require(sceSharePlayProhibitController(2) == notInitialized); Require(sceSharePlayInitialize(nullptr, sizeof(heap)) == 0); Require(sceSharePlayInitialize(heap, 0) == alreadyInitialized); @@ -41,6 +44,14 @@ int main() { Require(info.hostOnlineId.data[0] == 0 && info.visitorOnlineId.data[0] == 0 && info.hostOnlineId.pad[2] == 0); Require(info.hostUserId == -1 && info.visitorUserId == -1); Require(sceSharePlayGetCurrentConnectionInfoA(nullptr) == static_cast(0x810E0001)); + unsigned char plain[sizeof(SharePlayConnectionInfo) + 8]; + std::memset(plain, 0x5a, sizeof(plain)); + auto* plainInfo = reinterpret_cast(plain); + Require(sceSharePlayGetCurrentConnectionInfo(plainInfo) == 0); + Require(plainInfo->status == 0 && plainInfo->mode == 0 && plainInfo->hostOnlineId.data[0] == 0 && plainInfo->visitorOnlineId.pad[2] == 0); + Require(plainInfo->hostUserId == -1 && plainInfo->visitorUserId == -1); + for (std::size_t i = sizeof(SharePlayConnectionInfo); i < sizeof(plain); ++i) Require(plain[i] == 0x5a); + Require(sceSharePlayGetCurrentConnectionInfo(nullptr) == invalidArgument); Require(sceSharePlayProhibitController(1) == 0); Require(sceSharePlayProhibitController(1) == 0); Require(sceSharePlayProhibitController(0) == 0); diff --git a/core/libs/tests/GuestSignalHandlerMask.cpp b/core/libs/tests/GuestSignalHandlerMask.cpp new file mode 100644 index 0000000000..36029bfd31 --- /dev/null +++ b/core/libs/tests/GuestSignalHandlerMask.cpp @@ -0,0 +1,229 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +static constexpr int SigKill = 9; +static constexpr int SigTerm = 15; +static constexpr int SigUsr1 = 30; +static constexpr int SigUsr2 = 31; +static constexpr int SaRestart = 0x2; +static constexpr int SaResethand = 0x4; +static constexpr int SaNodefer = 0x10; +static constexpr int SigBlock = 1; +static constexpr int SigUnblock = 2; +static constexpr int SigSetmask = 3; + +static std::uint32_t Bit(int signal) { return 1u << (signal - 1); } + +static void Require(bool value) { if (!value) std::abort(); } + +static int calls = 0; +static int callsAfterNestedRaise = 0; +static std::uint32_t maskInHandler = 0; + +static std::uint32_t CurrentMask() { + GuestSignalSet set{}; + Require(sigprocmask_nid_postfix(SigBlock, nullptr, &set) == 0); + return set.bits[0]; +} + +static void APS5_VABI RaiseAgain(int signal) { + if (++calls == 1) { + maskInHandler = CurrentMask(); + Require(raise_nid_postfix(signal) == 0); + callsAfterNestedRaise = calls; + } +} + +static void APS5_VABI Count(int) { + ++calls; +} + +static std::uint64_t handlerExit[12]; + +static void APS5_VABI LeaveWithLongjmp(int) { + ++calls; + longjmp_nid_postfix(handlerExit, 1); +} + +static void APS5_VABI BlockFromOtherThread(int) { + ++calls; + std::thread([] { + const GuestSignalSet set{{Bit(SigTerm), 0, 0, 0}}; + Require(sigprocmask_nid_postfix(SigBlock, &set, nullptr) == 0); + }).join(); +} + +static void APS5_VABI SetMaskFromOtherThread(int) { + ++calls; + std::thread([] { + const GuestSignalSet set{{Bit(SigTerm), 0, 0, 0}}; + Require(sigprocmask_nid_postfix(SigSetmask, &set, nullptr) == 0); + }).join(); +} + +static void APS5_VABI SaveAndRestoreMask(int) { + ++calls; + GuestSignalSet old{}; + Require(sigprocmask_nid_postfix(SigBlock, nullptr, &old) == 0); + Require(sigprocmask_nid_postfix(SigSetmask, &old, nullptr) == 0); +} + +static void APS5_VABI ReplaceAndRestoreMask(int) { + ++calls; + const GuestSignalSet none{}; + GuestSignalSet old{}; + Require(sigprocmask_nid_postfix(SigSetmask, &none, &old) == 0); + Require(sigprocmask_nid_postfix(SigSetmask, &old, nullptr) == 0); +} + +static GuestSignalSet setInHandler{}; + +static void APS5_VABI RecordMask(int) { + ++calls; + Require(sigprocmask_nid_postfix(SigBlock, nullptr, &setInHandler) == 0); +} + +static void APS5_VABI UnblockAndRaise(int signal) { + if (++calls == 1) { + const GuestSignalSet own{{Bit(signal), 0, 0, 0}}; + Require(sigprocmask_nid_postfix(SigUnblock, &own, nullptr) == 0); + Require(raise_nid_postfix(signal) == 0); + callsAfterNestedRaise = calls; + } +} + +static void Install(int signal, void (APS5_VABI* handler)(int), int flags, std::uint32_t mask) { + GuestSigaction action{reinterpret_cast(handler), flags, {{mask, 0, 0, 0}}}; + Require(sigaction_nid_postfix(signal, &action, nullptr) == 0); +} + +static void CheckDeferred(int signal) { + calls = 0; + callsAfterNestedRaise = 0; + Install(signal, RaiseAgain, SaRestart, Bit(SigUsr1)); + Require(raise_nid_postfix(signal) == 0); + Require(callsAfterNestedRaise == 1); + Require(calls == 2); + Require(maskInHandler == (Bit(signal) | Bit(SigUsr1))); + Require(CurrentMask() == 0); +} + +static void CheckNodefer() { + calls = 0; + callsAfterNestedRaise = 0; + Install(SigUsr2, RaiseAgain, SaRestart | SaNodefer, 0); + Require(raise_nid_postfix(SigUsr2) == 0); + Require(callsAfterNestedRaise == 2); + Require(maskInHandler == 0); + Require(CurrentMask() == 0); +} + +static void CheckResethand() { + calls = 0; + Install(SigUsr1, Count, SaResethand, 0); + Require(raise_nid_postfix(SigUsr1) == 0); + Require(calls == 1); + GuestSigaction current{}; + Require(sigaction_nid_postfix(SigUsr1, nullptr, ¤t) == 0); + Require(current.handler == 0); + Require((current.flags & SaResethand) != 0); +} + +static void CheckOtherThreadKept() { + calls = 0; + Install(SigUsr1, BlockFromOtherThread, SaRestart, 0); + Require(raise_nid_postfix(SigUsr1) == 0); + Require(calls == 1); + Require(CurrentMask() == Bit(SigTerm)); + const GuestSignalSet set{{Bit(SigTerm), 0, 0, 0}}; + Require(sigprocmask_nid_postfix(SigUnblock, &set, nullptr) == 0); + Require(CurrentMask() == 0); +} + +static void CheckOtherThreadKeptSameSignal() { + calls = 0; + Install(SigUsr1, BlockFromOtherThread, SaRestart, Bit(SigTerm)); + Require(raise_nid_postfix(SigUsr1) == 0); + Require(calls == 1); + Require(CurrentMask() == Bit(SigTerm)); + const GuestSignalSet set{{Bit(SigTerm), 0, 0, 0}}; + Require(sigprocmask_nid_postfix(SigUnblock, &set, nullptr) == 0); + Require(CurrentMask() == 0); +} + +static void CheckOtherThreadSetMaskKept() { + calls = 0; + Install(SigUsr1, SetMaskFromOtherThread, SaRestart, 0); + Require(raise_nid_postfix(SigUsr1) == 0); + Require(calls == 1); + Require(CurrentMask() == Bit(SigTerm)); + const GuestSignalSet none{}; + Require(sigprocmask_nid_postfix(SigSetmask, &none, nullptr) == 0); + Require(CurrentMask() == 0); +} + +static void CheckMaskRestoredInsideHandler(int signal, void (APS5_VABI* handler)(int)) { + calls = 0; + Install(signal, handler, SaRestart, 0); + Require(raise_nid_postfix(signal) == 0); + Require(calls == 1); + Require(CurrentMask() == 0); + Require(raise_nid_postfix(signal) == 0); + Require(calls == 2); + Require(CurrentMask() == 0); +} + +static void CheckUnblockInsideHandler() { + calls = 0; + callsAfterNestedRaise = 0; + Install(SigUsr2, UnblockAndRaise, SaRestart, 0); + Require(raise_nid_postfix(SigUsr2) == 0); + Require(callsAfterNestedRaise == 2); + Require(CurrentMask() == 0); +} + +static void CheckUpperMaskWords() { + calls = 0; + const GuestSigaction action{reinterpret_cast(RecordMask), SaRestart, {{Bit(SigKill), 1u << 7, 0, 1u << 31}}}; + Require(sigaction_nid_postfix(SigUsr1, &action, nullptr) == 0); + Require(raise_nid_postfix(SigUsr1) == 0); + Require(calls == 1); + Require(setInHandler.bits[0] == Bit(SigUsr1) && setInHandler.bits[1] == 1u << 7 && setInHandler.bits[2] == 0 && setInHandler.bits[3] == 1u << 31); + GuestSignalSet after{}; + Require(sigprocmask_nid_postfix(SigBlock, nullptr, &after) == 0); + Require(after.bits[0] == 0 && after.bits[1] == 0 && after.bits[2] == 0 && after.bits[3] == 0); +} + +static void CheckLeftWithLongjmp() { + calls = 0; + Install(SigUsr1, LeaveWithLongjmp, SaRestart, Bit(SigUsr2)); + for (int round = 1; round <= 2; ++round) { + if (setjmp_nid_postfix(handlerExit) == 0) { + raise_nid_postfix(SigUsr1); + std::abort(); + } + Require(calls == round); + Require(CurrentMask() == 0); + } +} + +int main() { + CheckDeferred(SigUsr2); + CheckDeferred(SigTerm); + CheckNodefer(); + CheckResethand(); + CheckOtherThreadKept(); + CheckOtherThreadKeptSameSignal(); + CheckOtherThreadSetMaskKept(); + CheckMaskRestoredInsideHandler(SigUsr2, SaveAndRestoreMask); + CheckMaskRestoredInsideHandler(SigTerm, SaveAndRestoreMask); + CheckMaskRestoredInsideHandler(SigUsr2, ReplaceAndRestoreMask); + CheckUnblockInsideHandler(); + CheckUpperMaskWords(); + CheckLeftWithLongjmp(); + return 0; +} diff --git a/core/libs/tests/GuestSignalInterrupt.cpp b/core/libs/tests/GuestSignalInterrupt.cpp new file mode 100644 index 0000000000..d520e1954a --- /dev/null +++ b/core/libs/tests/GuestSignalInterrupt.cpp @@ -0,0 +1,98 @@ +#include "SceTypes.hpp" +#include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include + +using Handler = void (APS5_VABI *)(int); + +static_assert(sizeof(GuestSignalSet) == 16 && alignof(GuestSignalSet) == 4); +static_assert(sizeof(GuestSigaction) == 32 && offsetof(GuestSigaction, flags) == 8 && offsetof(GuestSigaction, mask) == 12); + +extern "C" { +extern GuestSignalSet _sigintr_nid_postfix; +} + +namespace { + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +bool Same(const GuestSigaction& first, const GuestSigaction& second) { + return first.handler == second.handler && first.flags == second.flags && + std::memcmp(&first.mask, &second.mask, sizeof(first.mask)) == 0; +} + +void APS5_VABI HandlerFunction(int) {} + +} + +int main() { + const GuestSignalSet initial = _sigintr_nid_postfix; + Require(initial.bits[0] == 0 && initial.bits[1] == 0 && initial.bits[2] == 0 && initial.bits[3] == 0, "initial interrupt set"); + for (int guest : {1, 32, 33, 64, 65, 96, 97, 128}) { + GuestSigaction original{}; + Require(sigaction_nid_postfix(guest, nullptr, &original) == 0, "read original disposition"); + GuestSigaction custom{reinterpret_cast(HandlerFunction), 0x42, + {{0x11111111, 0x22222222, 0x33333333, 0x44444444}}}; + Require(sigaction_nid_postfix(guest, &custom, nullptr) == 0, "install custom disposition"); + Require(sigaction_nid_postfix(guest, nullptr, &custom) == 0, "read installed disposition"); + const auto word = static_cast(guest - 1) / 32; + const auto bit = std::uint32_t{1} << (static_cast(guest - 1) % 32); + GuestSignalSet expected{{0x55555555, 0xaaaaaaaa, 0x55555555, 0xaaaaaaaa}}; + expected.bits[word] &= ~bit; + _sigintr_nid_postfix = expected; + for (int flag : {1, -1, std::numeric_limits::min(), std::numeric_limits::max()}) { + *__error_nid_postfix() = 12345; + Require(siginterrupt_nid_postfix(guest, flag) == 0, "nonzero flag"); + Require(*__error_nid_postfix() == 12345, "successful errno preservation"); + expected.bits[word] |= bit; + Require(std::memcmp(&_sigintr_nid_postfix, &expected, sizeof(expected)) == 0, "set bit without changing neighboring bits"); + GuestSigaction actual{}; + Require(sigaction_nid_postfix(guest, nullptr, &actual) == 0, "read interrupted disposition"); + auto interrupted = custom; + interrupted.flags &= ~0x2; + Require(Same(actual, interrupted), "preserve handler, mask and other flags"); + } + for (int repeat = 0; repeat < 2; ++repeat) { + *__error_nid_postfix() = 23456; + Require(siginterrupt_nid_postfix(guest, 0) == 0, "clear interrupt bit"); + Require(*__error_nid_postfix() == 23456, "zero flag errno preservation"); + expected.bits[word] &= ~bit; + Require(std::memcmp(&_sigintr_nid_postfix, &expected, sizeof(expected)) == 0, "clear bit without changing neighboring bits"); + GuestSigaction actual{}; + Require(sigaction_nid_postfix(guest, nullptr, &actual) == 0 && Same(actual, custom), "restore restart without changing disposition"); + } + Require(siginterrupt_nid_postfix(guest, -1) == 0, "make later signal interruptible"); + Require(signal_nid_postfix(guest, HandlerFunction) == HandlerFunction, "subsequent signal preserves previous handler"); + GuestSigaction actual{}; + Require(sigaction_nid_postfix(guest, nullptr, &actual) == 0 && actual.flags == 0, "signal consults shared interrupt set"); + Require(siginterrupt_nid_postfix(guest, 0) == 0, "make later signal restartable"); + Require(signal_nid_postfix(guest, HandlerFunction) == HandlerFunction, "repeat signal registration"); + Require(sigaction_nid_postfix(guest, nullptr, &actual) == 0 && actual.flags == 0x2, "signal restores restart"); + Require(sigaction_nid_postfix(guest, &original, nullptr) == 0, "restore original disposition"); + _sigintr_nid_postfix = initial; + } + const GuestSignalSet sentinels{{0x12345678, 0xabcdef01, 0x23456789, 0xbcdef012}}; + _sigintr_nid_postfix = sentinels; + GuestSigaction before{}; + Require(sigaction_nid_postfix(30, nullptr, &before) == 0, "read invalid-call control"); + for (int guest : {0, -1, 129, std::numeric_limits::min(), std::numeric_limits::max()}) { + for (int flag : {0, 1, -1}) { + *__error_nid_postfix() = 34567; + Require(siginterrupt_nid_postfix(guest, flag) == -1 && *__error_nid_postfix() == 22, "invalid signal reports EINVAL"); + Require(std::memcmp(&_sigintr_nid_postfix, &sentinels, sizeof(sentinels)) == 0, "invalid signal leaves all words unchanged"); + } + } + GuestSigaction after{}; + Require(sigaction_nid_postfix(30, nullptr, &after) == 0 && Same(before, after), "invalid calls preserve dispositions"); + _sigintr_nid_postfix = initial; +} diff --git a/core/libs/tests/GuestSignalSets.cpp b/core/libs/tests/GuestSignalSets.cpp index 4cce1b63a4..5440dc7032 100644 --- a/core/libs/tests/GuestSignalSets.cpp +++ b/core/libs/tests/GuestSignalSets.cpp @@ -154,6 +154,30 @@ static void CheckSigsetjmp() { SetBlockedMask(0); } +static int LongjumpBack(int value, std::uint32_t maskAtSet, std::uint32_t maskAtJump) { + std::uint64_t buffer[12]{}; + volatile int jumped = 0; + SetBlockedMask(maskAtSet); + const int result = setjmp_nid_postfix(buffer); + if (jumped == 0) { + Require(result == 0); + Require(BufferWord(buffer, 72) == maskAtSet); + Require(BufferWord(buffer, 88) == 1); + jumped = 1; + SetBlockedMask(maskAtJump); + longjmp_nid_postfix(buffer, value); + } + return result; +} + +static void CheckLongjmp() { + Require(LongjumpBack(0, 0x5u, 0x30u) == 1); + Require(BlockedMask() == 0x5u); + Require(LongjumpBack(7, 0x80000009u, 0u) == 7); + Require(BlockedMask() == 0x80000009u); + SetBlockedMask(0); +} + static void CheckInvalidHow() { SetBlockedMask(0x5u); const GuestSignalSet blocked{{0x30u, 0, 0, 0}}; @@ -175,6 +199,7 @@ static void CheckInvalidHow() { int main() { CheckSigsetjmp(); CheckSiglongjmp(); + CheckLongjmp(); CheckInvalidHow(); static constexpr int Untouched = 5; GuestSignalSet set{{0x12345678u, 0x9abcdef0u, 0xffffffffu, 1u}}; diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index 715ff0478c..8afec0ead8 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -231,7 +231,7 @@ int main() { received = 0; Require(raise_nid_postfix(30) == 0 && received == 0); Require(sigsuspend_nid_postfix(&suspendWithKillAndStop) == -1 && received == 30); - Require(maskInHandler.bits[0] == (1u << 28)); + Require(maskInHandler.bits[0] == ((1u << 28) | (1u << 29))); Require(sigprocmask_nid_postfix(1, nullptr, &previous) == 0 && previous.bits[0] == user.bits[0]); Require(signal_nid_postfix(30, Callback) != invalid); received = 0; diff --git a/core/libs/tests/GuestSigninDialog.cpp b/core/libs/tests/GuestSigninDialog.cpp index 0082e506f1..7baf28dae6 100644 --- a/core/libs/tests/GuestSigninDialog.cpp +++ b/core/libs/tests/GuestSigninDialog.cpp @@ -25,8 +25,10 @@ int main() { std::memset(result, 0x5a, sizeof(result)); Require(sceSigninDialogTerminate() == kErrNotInitialized); + Require(sceSigninDialogOpen(nullptr) == kErrNotInitialized); Require(sceSigninDialogOpen(¶m) == kErrNotInitialized); Require(sceSigninDialogClose() == kErrNotInitialized); + Require(sceSigninDialogGetResult(nullptr) == kErrNotInitialized); Require(sceSigninDialogGetResult(result) == kErrNotInitialized); Require(sceSigninDialogGetStatus() == kStatusNone); Require(sceSigninDialogUpdateStatus() == kStatusNone); @@ -36,6 +38,7 @@ int main() { Require(sceSigninDialogGetStatus() == kStatusInitialized); Require(sceSigninDialogOpen(nullptr) == kErrArgNull); + Require(sceSigninDialogGetResult(nullptr) == kErrNotFinished); Require(sceSigninDialogGetResult(result) == kErrNotFinished); Require(sceSigninDialogOpen(¶m) == 0); diff --git a/core/libs/tests/GuestSigtimedwait.cpp b/core/libs/tests/GuestSigtimedwait.cpp index 4c0101000c..8eb2de43af 100644 --- a/core/libs/tests/GuestSigtimedwait.cpp +++ b/core/libs/tests/GuestSigtimedwait.cpp @@ -2,9 +2,12 @@ #include "prx/libc/include/Api.hpp" #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include +#include #include #include #include +#include #include #include #include @@ -99,6 +102,24 @@ static void CheckTimeoutWithUnrelatedSignals() { Require(sigtimedwait_nid_postfix(&pipe, nullptr, &poll) == -1 && *__error_nid_postfix() == 35); Require(_sigprocmask_nid_postfix(3, &previous, nullptr) == 0); } +static void CheckShortTimeouts() { + const GuestSignalSet pipe{{1u << 12, 0, 0, 0}}; + for (const std::int64_t nanos : {100000, 1000000}) { + const KernelTimespec timeout{0, nanos}; + std::array samples{}; + for (auto& sample : samples) { + const auto before = std::chrono::steady_clock::now(); + Require(sigtimedwait_nid_postfix(&pipe, nullptr, &timeout) == -1 && *__error_nid_postfix() == 35); + sample = std::chrono::steady_clock::now() - before; + Require(sample >= std::chrono::nanoseconds(nanos)); + } + std::sort(samples.begin(), samples.end()); + const auto median = samples[samples.size() / 2]; + std::fprintf(stderr, "sigtimedwait(%lld ns) median: %lld ns\n", static_cast(nanos), + static_cast(median.count())); + Require(median < std::chrono::milliseconds(12)); + } +} int main() { const GuestSignalSet empty{{0, 0, 0, 0}}; const GuestSignalSet pipe{{1u << 12, 0, 0, 0}}; @@ -194,4 +215,5 @@ int main() { } Require(_sigprocmask_nid_postfix(3, &previous, nullptr) == 0); CheckTimeoutWithUnrelatedSignals(); + CheckShortTimeouts(); } diff --git a/core/libs/tests/GuestSocialScreenDialog.cpp b/core/libs/tests/GuestSocialScreenDialog.cpp index dcbb9c085e..fc75ebafcf 100644 --- a/core/libs/tests/GuestSocialScreenDialog.cpp +++ b/core/libs/tests/GuestSocialScreenDialog.cpp @@ -23,8 +23,10 @@ int main() { std::int32_t result[2] = {-1, -1}; Require(sceSocialScreenDialogTerminate() == kErrNotInitialized); + Require(sceSocialScreenDialogOpen(nullptr) == kErrNotInitialized); Require(sceSocialScreenDialogOpen(¶m) == kErrNotInitialized); Require(sceSocialScreenDialogClose() == kErrNotInitialized); + Require(sceSocialScreenDialogGetResult(nullptr) == kErrNotInitialized); Require(sceSocialScreenDialogGetResult(result) == kErrNotInitialized); Require(sceSocialScreenDialogGetStatus() == kStatusNone); Require(sceSocialScreenDialogUpdateStatus() == kStatusNone); diff --git a/core/libs/tests/GuestSocketAddressLength.cpp b/core/libs/tests/GuestSocketAddressLength.cpp index a95958f7a6..e2b7017eec 100644 --- a/core/libs/tests/GuestSocketAddressLength.cpp +++ b/core/libs/tests/GuestSocketAddressLength.cpp @@ -13,6 +13,7 @@ constexpr int Stream = 1; constexpr int Datagram = 2; constexpr int ErrorFault = 14; constexpr int ErrorInvalid = 22; +constexpr int ErrorAddressFamilyNotSupported = 47; constexpr int ErrorNameTooLong = 63; void Require(bool condition, const char* what) { @@ -44,6 +45,7 @@ void ShortAddressesAreInvalid() { const int stream6 = socket_nid_postfix(28, Stream, 0); Require(stream6 >= 0, "socket failed"); Require(Fails(connect_nid_postfix(stream6, v6.data(), 20), ErrorInvalid), "connect with an AF_INET6 address shorter than sockaddr_in6 must fail with EINVAL (22)"); + Require(Fails(bind_nid_postfix(stream6, v6.data(), 20), ErrorInvalid), "bind with an AF_INET6 address shorter than sockaddr_in6 must fail with EINVAL (22)"); Require(Fails(bind_nid_postfix(stream, address.data(), 256), ErrorNameTooLong), "an address longer than SOCK_MAXADDRLEN (255) must fail with ENAMETOOLONG (63)"); Require(Fails(bind_nid_postfix(stream, nullptr, address.size()), ErrorFault), "a null address must fail with EFAULT (14)"); Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0 && close_nid_postfix(stream6) == 0, "close failed"); @@ -62,6 +64,70 @@ void ArgumentsAreCheckedBeforeTheDescriptor() { Require(Fails(sendto_nid_postfix(-1, "x", 1, 0, address.data(), address.size()), ErrorBadDescriptor), "sendto(-1) with a valid address must fail with EBADF (9)"); } +void LongAddressesAndOtherFamiliesFail() { + const int datagram = socket_nid_postfix(2, Datagram, 0); + const int stream6 = socket_nid_postfix(28, Stream, 0); + const int datagram6 = socket_nid_postfix(28, Datagram, 0); + Require(datagram >= 0 && stream6 >= 0 && datagram6 >= 0, "socket failed"); + std::array address{16, 2}; + address[4] = 127; + address[7] = 1; + for (const std::uint32_t length : {17u, 28u, 255u}) { + Require(Fails(bind_nid_postfix(datagram, address.data(), length), ErrorInvalid), "bind with an AF_INET address longer than sockaddr_in must fail with EINVAL (22)"); + Require(Fails(connect_nid_postfix(datagram, address.data(), length), ErrorInvalid), "connect with an AF_INET address longer than sockaddr_in must fail with EINVAL (22)"); + Require(Fails(sendto_nid_postfix(datagram, "x", 1, 0, address.data(), length), ErrorInvalid), "sendto with an AF_INET address longer than sockaddr_in must fail with EINVAL (22)"); + } + std::array v6{28, 28}; + v6[23] = 1; + Require(Fails(bind_nid_postfix(stream6, v6.data(), 29), ErrorInvalid), "bind with an AF_INET6 address longer than sockaddr_in6 must fail with EINVAL (22)"); + Require(Fails(connect_nid_postfix(stream6, v6.data(), 64), ErrorInvalid), "connect with an AF_INET6 address longer than sockaddr_in6 must fail with EINVAL (22)"); + for (const std::uint8_t family : {std::uint8_t{28}, std::uint8_t{1}, std::uint8_t{0}}) { + address[1] = family; + Require(Fails(bind_nid_postfix(datagram, address.data(), 16), ErrorAddressFamilyNotSupported), "bind of another family on an AF_INET socket must fail with EAFNOSUPPORT (47)"); + } + address[1] = 28; + Require(Fails(sendto_nid_postfix(datagram, "x", 1, 0, address.data(), 28), ErrorInvalid), "sendto checks the length before the family"); + Require(Fails(sendto_nid_postfix(datagram, "x", 1, 0, address.data(), 16), ErrorAddressFamilyNotSupported), "sendto of another family fails with EAFNOSUPPORT (47)"); + address[1] = 2; + Require(Fails(bind_nid_postfix(datagram6, address.data(), 16), ErrorInvalid), "an AF_INET6 socket checks the length before the family"); + Require(Fails(connect_nid_postfix(datagram6, address.data(), 28), ErrorAddressFamilyNotSupported), "an AF_INET address of the AF_INET6 length fails with EAFNOSUPPORT (47)"); + Require(close_nid_postfix(datagram) == 0 && close_nid_postfix(stream6) == 0 && close_nid_postfix(datagram6) == 0, "close failed"); +} + +void ConnectedStreamSendtoIgnoresTheAddress() { + const int listener = socket_nid_postfix(2, Stream, 0); + const int client = socket_nid_postfix(2, Stream, 0); + Require(listener >= 0 && client >= 0, "socket failed"); + auto address = Loopback(); + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0 && listen_nid_postfix(listener, 1) == 0, "listen failed"); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &size) == 0, "getsockname failed"); + Require(connect_nid_postfix(client, address.data(), address.size()) == 0, "connect failed"); + std::array padded{}; + std::memcpy(padded.data(), address.data(), address.size()); + Require(sendto_nid_postfix(client, "x", 1, 0, padded.data(), padded.size()) == 1, "tcp_usr_send ignores the address of a connected stream socket, so sendto must send"); + Require(Fails(sendto_nid_postfix(client, "x", 1, 0, padded.data(), 1), ErrorInvalid), "getsockaddr still checks the length of a connected stream socket's address"); + Require(close_nid_postfix(client) == 0 && close_nid_postfix(listener) == 0, "close failed"); +} + +void MismatchedAndUnknownFamiliesFail() { + const int stream = socket_nid_postfix(2, Stream, 0); + const int datagram = socket_nid_postfix(2, Datagram, 0); + const int stream6 = socket_nid_postfix(28, Stream, 0); + const int datagram6 = socket_nid_postfix(28, Datagram, 0); + Require(stream >= 0 && datagram >= 0 && stream6 >= 0 && datagram6 >= 0, "socket failed"); + const auto address = Loopback(); + Require(Fails(bind_nid_postfix(stream6, address.data(), address.size()), ErrorInvalid), "bind with an AF_INET address on an AF_INET6 socket must fail with EINVAL (22)"); + std::array unknownFamily{16, 99}; + Require(Fails(connect_nid_postfix(stream, unknownFamily.data(), unknownFamily.size()), ErrorAddressFamilyNotSupported), "connect with an unknown family must fail with EAFNOSUPPORT (47)"); + Require(Fails(sendto_nid_postfix(datagram, "x", 1, 0, unknownFamily.data(), unknownFamily.size()), ErrorAddressFamilyNotSupported), "sendto with an unknown family must fail with EAFNOSUPPORT (47)"); + std::array unknownFamily6{28, 99}; + Require(Fails(bind_nid_postfix(stream6, unknownFamily6.data(), unknownFamily6.size()), ErrorAddressFamilyNotSupported), "bind with an unknown family must fail with EAFNOSUPPORT (47)"); + Require(Fails(connect_nid_postfix(stream6, unknownFamily6.data(), unknownFamily6.size()), ErrorAddressFamilyNotSupported), "connect with an unknown family must fail with EAFNOSUPPORT (47)"); + Require(Fails(sendto_nid_postfix(datagram6, "x", 1, 0, unknownFamily6.data(), unknownFamily6.size()), ErrorAddressFamilyNotSupported), "sendto with an unknown family must fail with EAFNOSUPPORT (47)"); + Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0 && close_nid_postfix(stream6) == 0 && close_nid_postfix(datagram6) == 0, "close failed"); +} + void FullAddressesStillWork() { const int stream = socket_nid_postfix(2, Stream, 0); Require(stream >= 0, "socket failed"); @@ -123,6 +189,8 @@ void LongMappedAddressesBindOnlyDualStackDatagramSockets() { Require(setsockopt_nid_postfix(datagram, 41, 27, &off, sizeof(off)) == 0, "clearing IPV6_V6ONLY failed"); Require(bind_nid_postfix(datagram, mapped.data(), mapped.size()) == 0, "udp6_bind passes a long IPv4-mapped address to in_pcbbind without IPV6_V6ONLY, so bind must succeed"); Require(Fails(sendto_nid_postfix(datagram, "x", 1, 0, mapped.data(), mapped.size()), ErrorInvalid), "udp6_send checks the length before the IPv4-mapped case, so sendto must fail with EINVAL (22)"); + mapped[3] = 9; + Require(connect_nid_postfix(datagram, mapped.data(), mapped.size()) == 0, "udp6_connect passes a long IPv4-mapped address to in_pcbconnect without IPV6_V6ONLY, so connect must succeed"); Require(close_nid_postfix(datagram) == 0, "close failed"); } @@ -131,6 +199,9 @@ void LongMappedAddressesBindOnlyDualStackDatagramSockets() { int main() { ShortAddressesAreInvalid(); ArgumentsAreCheckedBeforeTheDescriptor(); + LongAddressesAndOtherFamiliesFail(); + ConnectedStreamSendtoIgnoresTheAddress(); + MismatchedAndUnknownFamiliesFail(); FullAddressesStillWork(); LongAddressesAreInvalid(2, Stream); LongAddressesAreInvalid(2, Datagram); diff --git a/core/libs/tests/GuestSocketOptions.cpp b/core/libs/tests/GuestSocketOptions.cpp index f100a443a7..7c73a6f364 100644 --- a/core/libs/tests/GuestSocketOptions.cpp +++ b/core/libs/tests/GuestSocketOptions.cpp @@ -317,6 +317,11 @@ void IpLevelFollowsThePs5() { size = sizeof(value); Require(getsockopt_nid_postfix(datagram, 99, 1, &value, &size) == -1 && *__error_nid_postfix() == ErrorInvalid, "an unknown level must fail with EINVAL (22)"); Require(SetFails(datagram, 99, 1, &value, sizeof(value), ErrorInvalid), "setsockopt at an unknown level must fail with EINVAL (22)"); + for (const int option : {7, 20, 65}) { + Require(GetIp(datagram, option) == 0, "a receive option starts off"); + Require(SetIp(datagram, option, 5) && GetIp(datagram, option) == 1, "a receive option reads back as 1"); + Require(SetIp(datagram, option, 0) && GetIp(datagram, option) == 0, "a receive option turns off"); + } Require(close_nid_postfix(datagram) == 0 && close_nid_postfix(datagram6) == 0, "close failed"); } diff --git a/core/libs/tests/GuestSocketPair.cpp b/core/libs/tests/GuestSocketPair.cpp index 9f865cfa4b..ccfd0fc4c3 100644 --- a/core/libs/tests/GuestSocketPair.cpp +++ b/core/libs/tests/GuestSocketPair.cpp @@ -65,6 +65,14 @@ static bool Unnamed(int descriptor, int (APS5_VABI *query)(int, void*, std::uint return query(descriptor, name, &length) == 0 && length == 4 && name[0] == 16 && name[1] == 1 && name[4] == 0xaa; } +static bool NotConnected(int descriptor) { + std::uint8_t name[16]; + std::memset(name, 0xaa, sizeof(name)); + std::uint32_t length = sizeof(name); + *__error_nid_postfix() = 0; + return getpeername_nid_postfix(descriptor, name, &length) == -1 && *__error_nid_postfix() == 57 && length == sizeof(name) && name[0] == 0xaa; +} + static bool ReceiveFaults(int descriptor, GuestMsghdr* message) { *__error_nid_postfix() = 0; return recvmsg_nid_postfix(descriptor, message, 0) == -1 && *__error_nid_postfix() == 14; @@ -172,7 +180,9 @@ int main(int argc, char** argv) { Require(shutdown_nid_postfix(pair[0], 1) == 0); Require(Readable(pair[1], In | Hup) && recv_nid_postfix(pair[1], received, sizeof(received), 0) == 0); Require(Exchange(pair[1], pair[0], "still open")); + Require(Unnamed(pair[0], getpeername_nid_postfix) && Unnamed(pair[1], getpeername_nid_postfix)); Require(close_nid_postfix(pair[0]) == 0); + Require(NotConnected(pair[1]) && Unnamed(pair[1], getsockname_nid_postfix)); Require(Readable(pair[1], In | Hup) && recv_nid_postfix(pair[1], received, sizeof(received), 0) == 0); std::int64_t sent = 0; for (int i = 0; i < 100 && sent >= 0; ++i) sent = send_nid_postfix(pair[1], ping, sizeof(ping), NoSignal); @@ -234,7 +244,11 @@ int main(int argc, char** argv) { Require(senderLength == 16 && sender[0] == 16 && sender[1] == 1); Require(recv_nid_postfix(pair[1], datagram, sizeof(datagram), 0) == 2 && datagram[0] == 'b' && datagram[1] == 'b'); Require(Exchange(pair[1], pair[0], "back")); - Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + const int copy = dup_nid_postfix(pair[0]); + Require(copy >= 0 && close_nid_postfix(pair[0]) == 0); + Require(Unnamed(pair[1], getpeername_nid_postfix) && Exchange(pair[1], copy, "copy")); + Require(close_nid_postfix(copy) == 0 && NotConnected(pair[1])); + Require(close_nid_postfix(pair[1]) == 0); #ifdef __APPLE__ Require(ThrowsRuntimeError([&] { socketpair_nid_postfix(Unix, SequencedPacket, 0, pair); })); diff --git a/core/libs/tests/GuestSocketStreamOptions.cpp b/core/libs/tests/GuestSocketStreamOptions.cpp index 63ba1ec240..647c93cdda 100644 --- a/core/libs/tests/GuestSocketStreamOptions.cpp +++ b/core/libs/tests/GuestSocketStreamOptions.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include @@ -74,13 +75,14 @@ int main() { Require(client >= 0 && connect_nid_postfix(client, address.data(), address.size()) == 0); const int server = accept_nid_postfix(listener, nullptr, nullptr); Require(server >= 0); - Require(Set(listener, SocketLevel, NoSigpipe, 1) && Set(listener, TcpLevel, TcpNoDelay, 1) && Set(listener, TcpLevel, 0x100, 60)); + Require(Set(listener, SocketLevel, NoSigpipe, 1) && Set(listener, TcpLevel, TcpNoDelay, 1) && Set(listener, TcpLevel, 0x100, 60) && Set(listener, TcpLevel, 4, 1) && Set(listener, TcpLevel, 8, 1)); const int secondClient = socket_nid_postfix(2, 1, 0); Require(secondClient >= 0 && connect_nid_postfix(secondClient, address.data(), address.size()) == 0); const int inheriting = accept_nid_postfix(listener, nullptr, nullptr); Require(inheriting >= 0); Require(Get(inheriting, SocketLevel, NoSigpipe) == NoSigpipe && Get(inheriting, TcpLevel, TcpNoDelay) == 4); Require(Get(inheriting, TcpLevel, 0x100) == 60 && Get(server, TcpLevel, 0x100) == 7200); + Require(Get(inheriting, TcpLevel, 4) == 0x1000 && Get(inheriting, TcpLevel, 8) == 0); Require(Get(server, SocketLevel, NoSigpipe) == 0 && Get(server, TcpLevel, TcpNoDelay) == 0); Require(close_nid_postfix(inheriting) == 0 && close_nid_postfix(secondClient) == 0); const int datagram = socket_nid_postfix(2, 2, 0); @@ -104,7 +106,18 @@ int main() { for (const int segment : {1000, 175, 0, -1}) { Require(Fails(setsockopt_nid_postfix(fresh, TcpLevel, TcpMaximumSegment, &segment, sizeof(segment)), Invalid)); } - Require(Set(fresh, TcpLevel, TcpMaximumSegment, 300) && Get(fresh, TcpLevel, TcpMaximumSegment) == 300); + for (const int segment : {176, 300, 536}) + Require(Fails(setsockopt_nid_postfix(fresh, TcpLevel, TcpMaximumSegment, &segment, sizeof(segment)), Invalid)); + Require(Get(fresh, TcpLevel, TcpMaximumSegment) == 536); + const int fresh6 = socket_nid_postfix(28, 1, 0); + if (fresh6 >= 0) { + Require(Get(fresh6, TcpLevel, TcpMaximumSegment) == 1220); + Require(close_nid_postfix(fresh6) == 0); + } + Require(Set(client, TcpLevel, TcpMaximumSegment, 176) && Get(client, TcpLevel, TcpMaximumSegment) == 216); + Require(Set(client, TcpLevel, TcpMaximumSegment, 200) && Get(client, TcpLevel, TcpMaximumSegment) == 216); + const int larger = 400; + Require(Fails(setsockopt_nid_postfix(client, TcpLevel, TcpMaximumSegment, &larger, sizeof(larger)), Invalid)); Require(Get(fresh, TcpLevel, 0x100) == 7200 && Get(fresh, TcpLevel, 0x200) == 75 && Get(fresh, TcpLevel, 0x400) == 8); Require(Set(fresh, TcpLevel, 0x100, 60) && Get(fresh, TcpLevel, 0x100) == 60); Require(Set(fresh, TcpLevel, 0x100, 0) && Get(fresh, TcpLevel, 0x100) == 7200); @@ -112,6 +125,12 @@ int main() { Require(Set(fresh, TcpLevel, 0x400, 3) && Get(fresh, TcpLevel, 0x400) == 3); Require(Set(fresh, TcpLevel, 0x400, -1) && Get(fresh, TcpLevel, 0x400) == -1); Require(Fails(setsockopt_nid_postfix(fresh, TcpLevel, 0x7777, &one, sizeof(one)), NoProtocolOption)); + for (const auto& [option, bit] : {std::pair{4, 0x1000}, std::pair{8, 0x8}, std::pair{0x80, 0x10000}}) { + Require(Get(fresh, TcpLevel, option) == 0); + Require(Set(fresh, TcpLevel, option, 5) && Get(fresh, TcpLevel, option) == bit); + Require(Set(fresh, TcpLevel, option, -1) && Get(fresh, TcpLevel, option) == bit); + Require(Set(fresh, TcpLevel, option, 0) && Get(fresh, TcpLevel, option) == 0); + } Require(close_nid_postfix(fresh) == 0); length = sizeof(value); Require(Fails(setsockopt_nid_postfix(datagram, TcpLevel, TcpNoDelay, &one, sizeof(one)), Invalid)); diff --git a/core/libs/tests/GuestSpSysCallWrapper.cpp b/core/libs/tests/GuestSpSysCallWrapper.cpp new file mode 100644 index 0000000000..12f2192bc4 --- /dev/null +++ b/core/libs/tests/GuestSpSysCallWrapper.cpp @@ -0,0 +1,161 @@ +#include "SceTypes.hpp" +#include "prx/libSceSpSysCallWrapper/libSceSpSysCallWrapper.hpp" +#include "prx/libSceNet/libSceNet.hpp" +#include "prx/libkernel/libkernel.hpp" +#include +#include +#include + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +struct Shared { + PthreadMutex mutex = nullptr; + PthreadCond cond = nullptr; + bool ready = false; +}; + +void* APS5_VABI ExitWithArgument(void* arg) { + sceSpPthreadExit(arg); + return nullptr; +} + +void* APS5_VABI SignalReady(void* arg) { + auto* shared = static_cast(arg); + Require(sceSpPthreadMutexLock(&shared->mutex) == 0); + shared->ready = true; + Require(sceSpPthreadCondSignal(&shared->cond) == 0); + Require(sceSpPthreadMutexUnlock(&shared->mutex) == 0); + return nullptr; +} + +void Threads() { + PthreadAttr attr = nullptr; + Require(sceSpPthreadAttrInit(&attr) == 0); + Require(sceSpPthreadAttrSetstacksize(&attr, 0x10000) == 0); + Require(sceSpPthreadAttrSetinheritsched(&attr, 0) == scePthreadAttrSetinheritsched(&attr, 0)); + const KernelSchedParam param{700}; + Require(sceSpPthreadAttrSetschedparam(&attr, ¶m) == scePthreadAttrSetschedparam(&attr, ¶m)); + + Pthread thread = nullptr; + int marker = 0; + Require(sceSpPthreadCreate(&thread, &attr, ExitWithArgument, &marker, "sp_exit") == 0); + void* result = nullptr; + Require(sceSpPthreadJoin(thread, &result) == 0); + Require(result == &marker); + + Pthread detached = nullptr; + Require(sceSpPthreadCreate(&detached, &attr, ExitWithArgument, nullptr, "sp_detach") == 0); + Require(sceSpPthreadDetach(detached) == 0); + Require(sceSpPthreadAttrDestroy(&attr) == 0); + + PthreadMutexattr mutexAttr = nullptr; + Require(sceSpPthreadMutexattrInit(&mutexAttr) == 0); + Require(sceSpPthreadMutexattrSettype(&mutexAttr, 2) == 0); + Require(sceSpPthreadMutexattrSetprotocol(&mutexAttr, 0) == scePthreadMutexattrSetprotocol(&mutexAttr, 0)); + Require(sceSpPthreadSetprio(scePthreadSelf(), 700) == scePthreadSetprio(scePthreadSelf(), 700)); + PthreadMutexattr ceilingAttr = nullptr; + Require(sceSpPthreadMutexattrInit(&ceilingAttr) == 0); + Require(sceSpPthreadMutexattrSetprioceiling(&ceilingAttr, 300) == static_cast(0x80020016u)); + Require(sceSpPthreadMutexattrSetprotocol(&ceilingAttr, 2) == 0); + Require(sceSpPthreadMutexattrSetprioceiling(&ceilingAttr, 300) == 0); + int ceiling = 0; + Require(scePthreadMutexattrGetprioceiling(&ceilingAttr, &ceiling) == 0 && ceiling == 300); + Require(sceSpPthreadMutexattrDestroy(&ceilingAttr) == 0); + PthreadCondattr condAttr = nullptr; + Require(sceSpPthreadCondattrInit(&condAttr) == 0); + + Shared shared; + Require(sceSpPthreadMutexInit(&shared.mutex, &mutexAttr, "sp_mutex") == 0); + Require(sceSpPthreadCondInit(&shared.cond, &condAttr, "sp_cond") == 0); + Require(sceSpPthreadMutexattrDestroy(&mutexAttr) == 0); + Require(sceSpPthreadCondattrDestroy(&condAttr) == 0); + + Require(sceSpPthreadMutexLock(&shared.mutex) == 0); + Pthread signaller = nullptr; + Require(sceSpPthreadCreate(&signaller, nullptr, SignalReady, &shared, "sp_signal") == 0); + while (!shared.ready) Require(sceSpPthreadCondWait(&shared.cond, &shared.mutex) == 0); + Require(sceSpPthreadMutexUnlock(&shared.mutex) == 0); + Require(sceSpPthreadJoin(signaller, nullptr) == 0); + Require(sceSpPthreadCondDestroy(&shared.cond) == 0); + Require(sceSpPthreadMutexDestroy(&shared.mutex) == 0); +} + +void Time() { + KernelTimeval now{}; + Require(sceSpKernelGettimeofday(&now) == 0); + Require(now.tv_sec > 0 && now.tv_usec >= 0 && now.tv_usec < 1000000); + const KernelTimespec delay{0, 1000000}; + Require(sceSpKernelNanosleep(&delay, nullptr) == 0); +} + +template +void SameError(TWrapped wrapped, TDirect direct) { + const auto wrappedResult = wrapped(); + const int wrappedErrno = *sceNetErrnoLoc(); + const auto directResult = direct(); + Require(wrappedResult == directResult); + Require(wrappedErrno == *sceNetErrnoLoc()); +} + +void Net() { + Require(sceSpNetErrnoLoc() == sceNetErrnoLoc()); + + const int pool = sceSpNetPoolCreate("sp_pool", 0x4000, 0); + Require(pool >= 0); + const int resolver = sceSpNetResolverCreate("sp_resolver", pool, 0); + Require(resolver >= 0); + int status = 0; + Require(sceSpNetResolverGetError(resolver, &status) == sceNetResolverGetError(resolver, &status)); + Require(sceSpNetResolverDestroy(resolver) == 0); + Require(sceSpNetPoolDestroy(pool) == 0); + + const int epoll = sceSpNetEpollCreate("sp_epoll", 0); + Require(epoll >= 0); + Require(sceSpNetEpollDestroy(epoll) == 0); + + const int socket = sceSpNetSocket("sp_socket", 2, 2, 0); + Require(socket >= 0); + const int enable = 1; + Require(sceSpNetSetsockopt(socket, 0xffff, 0x4, &enable, sizeof(enable)) == sceNetSetsockopt(socket, 0xffff, 0x4, &enable, sizeof(enable))); + Require(sceSpNetSocketClose(socket) == 0); + + char byte = 0; + const std::uint8_t address[16]{16, 2}; + SameError([&] { return sceSpNetSend(-1, &byte, 1, 0); }, [&] { return sceNetSend(-1, &byte, 1, 0); }); + SameError([&] { return sceSpNetRecv(-1, &byte, 1, 0); }, [&] { return sceNetRecv(-1, &byte, 1, 0); }); + SameError([&] { return sceSpNetConnect(-1, address, sizeof(address)); }, [&] { return sceNetConnect(-1, address, sizeof(address)); }); + SameError([&] { return sceSpNetEpollControl(-1, 1, 0, nullptr); }, [&] { return sceNetEpollControl(-1, 1, 0, nullptr); }); + SameError([&] { return sceSpNetEpollWait(-1, nullptr, 0, 0); }, [&] { return sceNetEpollWait(-1, nullptr, 0, 0); }); + SameError([&] { return sceSpNetEpollAbort(-1, 0); }, [&] { return sceNetEpollAbort(-1, 0); }); + SameError([&] { return sceSpNetResolverAbort(-1, 0); }, [&] { return sceNetResolverAbort(-1, 0); }); + std::uint32_t resolved = 0; + SameError([&] { return sceSpNetResolverStartNtoa(-1, "localhost", &resolved, 0, 0, 0); }, [&] { return sceNetResolverStartNtoa(-1, "localhost", &resolved, 0, 0, 0); }); +} + +void Posix() { + Require(sceSpGetPid() > 0); + Require(sceSpGetPid() == getpid_nid_postfix()); + + const std::int64_t zero[2]{0, 0}; + Require(sceSpSelect(0, nullptr, nullptr, nullptr, zero) == 0); + const std::int64_t invalid[2]{0, 1000000}; + *__error_nid_postfix() = 0; + Require(sceSpSelect(0, nullptr, nullptr, nullptr, invalid) == -1); + const int wrappedErrno = *__error_nid_postfix(); + *__error_nid_postfix() = 0; + Require(select_nid_postfix(0, nullptr, nullptr, nullptr, invalid) == -1); + Require(wrappedErrno == 22 && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(sceSpSelect(-1, nullptr, nullptr, nullptr, nullptr) == -1 && *__error_nid_postfix() == 22); +} + +} + +int main() { + Threads(); + Time(); + Net(); + Posix(); +} diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 0f459b1e83..3b0dd20541 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/Api.hpp" #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -472,7 +473,13 @@ static void CheckDescriptorAccessModes() { #endif Require(descriptor >= 0); const int requested = std::strchr(mode, '+') ? 2 : *mode == 'r' ? 0 : 1; +#ifdef _WIN32 + const bool incompatible = false; + const bool unchecked = access != 2 && access != requested; +#else const bool incompatible = access != 2 && access != requested; + const bool unchecked = false; +#endif #ifdef _WIN32 const auto originalHandle = _get_osfhandle(descriptor); if (incompatible) Require(_lseek(descriptor, 2, SEEK_SET) == 2); @@ -503,6 +510,16 @@ static void CheckDescriptorAccessModes() { Require(::close(descriptor) == 0); #endif Require(CheckFileBytes(filename, probeAppend ? "reXained" : "retained", mode, false)); + } else if (unchecked) { + Require(stream != nullptr && fileno_nid_postfix(stream) == descriptor); + if (access == 1) { + Require(fgetc_nid_postfix(stream) == -1 && ferror_nid_postfix(stream) != 0); + } else { + Require(fputc_nid_postfix('X', stream) == 'X'); + Require(fflush_nid_postfix(stream) == -1 && *__error_nid_postfix() == 9); + } + fclose_nid_postfix(stream); + Require(CheckFileBytes(filename, "retained", mode, false)); } else { Require(stream != nullptr); if (requested == 0) Require(fgetc_nid_postfix(stream) == 'r'); @@ -539,6 +556,16 @@ static void CheckClosedStreamWrites() { Require(fseeko_nid_postfix(&stream, 0, SEEK_SET) == -1 && *__error_nid_postfix() == 9); Require(ftello_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); Require(fileno_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(fputwc_nid_postfix(u't', &stream) == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(putwc_nid_postfix(u't', &stream) == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(fputws_nid_postfix(u"text", &stream) == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(ungetwc_nid_postfix('t', &stream) == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(ungetwc_nid_postfix(0x100, &stream) == -1 && *__error_nid_postfix() == 9); Require(ferror_nid_postfix(&stream) == 0 && feof_nid_postfix(&stream) == 0); Require(fclose_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); } @@ -549,6 +576,10 @@ static int CheckClosedStandardOutput() { Require(puts_nid_postfix("text") == -1 && *__error_nid_postfix() == 9); Require(PrintStandard("%d", 1) == -1 && *__error_nid_postfix() == 9); Require(fputs_nid_postfix("text", __stdoutp_nid_postfix) == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(wprintf_nid_postfix(u"%d", 1) == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(putwchar_nid_postfix(u't') == -1 && *__error_nid_postfix() == 9); Require(std::fputs("host output\n", stdout) >= 0 && std::fflush(stdout) == 0 && !std::ferror(stdout)); Require(freopen_nid_postfix("missing-closed-stream-dir/err.log", "a", __stderrp_nid_postfix) == nullptr); Require(std::fputs("host error\n", stderr) >= 0 && std::fflush(stderr) == 0 && !std::ferror(stderr)); @@ -592,7 +623,14 @@ int main(int argc, char** argv) { CheckDescriptorAccessModes(); Require(_Getmbcurmax_nid_postfix() == 3 && ___mb_cur_max_nid_postfix() == 1); CheckFlushWhileReading(); - Require(fdopen_nid_postfix(-1, "rb") == nullptr && *__error_nid_postfix() == 9); + for (const int negative : {-1, -2, INT_MIN}) { + *__error_nid_postfix() = 1234; + Require(fdopen_nid_postfix(negative, "rb") == nullptr && *__error_nid_postfix() == 1234); + for (const char* invalid : {"x", "", "z", "xr"}) { + *__error_nid_postfix() = 1234; + Require(fdopen_nid_postfix(negative, invalid) == nullptr && *__error_nid_postfix() == 22); + } + } Require(fdopen_nid_postfix(0, nullptr) == nullptr && *__error_nid_postfix() == 22); Require(fdopen_nid_postfix(0, "invalid") == nullptr && *__error_nid_postfix() == 22); std::FILE* original = std::tmpfile(); diff --git a/core/libs/tests/GuestStdioVariadic.cpp b/core/libs/tests/GuestStdioVariadic.cpp index 0fa24de842..80d9e2d166 100644 --- a/core/libs/tests/GuestStdioVariadic.cpp +++ b/core/libs/tests/GuestStdioVariadic.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/Api.hpp" #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -41,6 +42,59 @@ static int APS5_VABI ScanInput(const char* format, ...) { return result; } +static int APS5_VABI PrintSecure(FileStream* stream, const char* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfprintf_s_nid_postfix(stream, format, reinterpret_cast(args)); + APS5_VA_END(); + return result; +} + +static int APS5_VABI PrintSecureOutput(const char* format, ...) { + APS5_VA_BEGIN(format); + const int result = vprintf_s_nid_postfix(format, reinterpret_cast(args)); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanSecureStream(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 ScanSecureInput(const char* format, ...) { + APS5_VA_BEGIN(format); + const int result = vscanf_s_nid_postfix(format, reinterpret_cast(args)); + APS5_VA_END(); + return result; +} + +static int APS5_VABI PrintWideSecure(FileStream* stream, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfwprintf_s_nid_postfix(stream, format, reinterpret_cast(args)); + APS5_VA_END(); + return result; +} + +static int APS5_VABI PrintWideSecureOutput(const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vwprintf_s_nid_postfix(format, reinterpret_cast(args)); + APS5_VA_END(); + return result; +} + +static FileStream* SeededStream(const char* text) { + auto* stream = new FileStream(std::tmpfile(), true); + Require(std::fputs(text, stream->GetHandle()) >= 0, "seed a stream"); + std::rewind(stream->GetHandle()); + return stream; +} + +static int NextByte(FileStream* stream) { + return std::fgetc(stream->GetHandle()); +} + static int APS5_VABI PrintWide(FileStream* stream, const char16_t* format, ...) { APS5_VA_BEGIN(format); const int result = vfwprintf_nid_postfix(stream, format, reinterpret_cast(args)); @@ -128,9 +182,108 @@ int main() { FileStream written(std::tmpfile()); Require(fwprintf_nid_postfix(&written, u"%d:%ls|", 7, u"été") == 6, "fwprintf counts wide characters"); Require(PrintWide(&written, u"%5.1f", 2.25) == 5, "vfwprintf counts wide characters"); - Require(Contents(written) == "7:\xc3\xa9t\xc3\xa9| 2.2", "the stream holds the formatted text"); + Require(Contents(written) == "7:\xc3\xa9t\xc3\xa9| 2.3", "the stream holds the formatted text"); written.Close(); + FileStream securePlain(std::tmpfile()); + FileStream secureChecked(std::tmpfile()); + FileStream secureList(std::tmpfile()); + Require(fprintf_nid_postfix(&securePlain, "%d|%s|%5.2f|%c|%ls", 42, "ab", 3.14159, 'z', u"é") == 16, "fprintf reference"); + Require(fprintf_s_nid_postfix(&secureChecked, "%d|%s|%5.2f|%c|%ls", 42, "ab", 3.14159, 'z', u"é") == 16, "fprintf_s count"); + Require(PrintSecure(&secureList, "%d|%s|%5.2f|%c|%ls", 42, "ab", 3.14159, 'z', u"é") == 16, "vfprintf_s count"); + const std::string expectedText = "42|ab| 3.14|z|\xc3\xa9"; + Require(Contents(securePlain) == expectedText, "fprintf reference bytes"); + Require(Contents(secureChecked) == expectedText, "fprintf_s writes the bytes fprintf writes"); + Require(Contents(secureList) == expectedText, "vfprintf_s writes the bytes fprintf writes"); + securePlain.Close(); + secureChecked.Close(); + secureList.Close(); + + FileStream violations(std::tmpfile()); + signed char marker = -7; + char sibling[16]; + Require(snprintf_s_nid_postfix(sibling, sizeof(sibling), "ab%hhn", &marker) == -1, "snprintf_s rejects %n"); + Require(fprintf_s_nid_postfix(&violations, "ab%hhn", &marker) == -1 && marker == -7, "fprintf_s rejects %n like snprintf_s"); + Require(PrintSecure(&violations, "ab%n", &number) == -1, "vfprintf_s rejects %n"); + Require(snprintf_s_nid_postfix(sibling, sizeof(sibling), "%s", static_cast(nullptr)) == -1, "snprintf_s rejects a null %s"); + Require(fprintf_s_nid_postfix(&violations, "x%s", static_cast(nullptr)) == -1, "fprintf_s rejects a null %s"); + Require(fprintf_s_nid_postfix(&violations, static_cast(nullptr)) == -1, "fprintf_s rejects a null format"); + Require(PrintSecure(&violations, static_cast(nullptr)) == -1, "vfprintf_s rejects a null format"); + Require(fprintf_s_nid_postfix(nullptr, "x") == -1, "fprintf_s rejects a null stream"); + Require(PrintSecure(nullptr, "x") == -1, "vfprintf_s rejects a null stream"); + Require(Contents(violations).empty(), "a rejected call writes nothing"); + Require(fprintf_s_nid_postfix(&violations, "%s", "") == 0, "fprintf_s of an empty string"); + violations.Close(); + errno = 0; + Require(fprintf_s_nid_postfix(&violations, "x") == -1 && errno == 9, "fprintf_s on a closed stream sets EBADF"); + Require(PrintSecureOutput("vprintf_s: %d\n", 42) == 14, "vprintf_s counts the bytes written to standard output"); + Require(PrintSecureOutput("%n", &number) == -1 && PrintSecureOutput(static_cast(nullptr)) == -1, "vprintf_s violations"); + + FileStream wideChecked(std::tmpfile()); + FileStream wideList(std::tmpfile()); + int widePosition = -5; + Require(fwprintf_s_nid_postfix(&wideChecked, u"%05d:%ls|", 42, u"été") == 10, "fwprintf_s counts wide characters"); + Require(PrintWideSecure(&wideList, u"%05d:%ls|", 42, u"été") == 10, "vfwprintf_s counts wide characters"); + Require(Contents(wideChecked) == "00042:\xc3\xa9t\xc3\xa9|" && Contents(wideList) == Contents(wideChecked), "wide secure output bytes"); + Require(fwprintf_s_nid_postfix(&wideChecked, u"a%n", &widePosition) == -1 && widePosition == -5, "fwprintf_s rejects %n"); + Require(PrintWideSecure(&wideChecked, u"a%ls", static_cast(nullptr)) == -1, "vfwprintf_s rejects a null %ls"); + Require(fwprintf_s_nid_postfix(&wideChecked, static_cast(nullptr)) == -1, "fwprintf_s rejects a null format"); + Require(fwprintf_s_nid_postfix(nullptr, u"x") == -1 && PrintWideSecure(nullptr, u"x") == -1, "wide secure forms reject a null stream"); + wideChecked.Close(); + wideList.Close(); + errno = 0; + Require(fwprintf_s_nid_postfix(&wideList, u"x") == -1 && errno == 9, "fwprintf_s on a closed stream sets EBADF"); + Require(wprintf_s_nid_postfix(u"wprintf_s %d\n", 5) == 12, "wprintf_s counts wide characters"); + Require(PrintWideSecureOutput(u"vwprintf_s %d\n", 6) == 13, "vwprintf_s counts wide characters"); + Require(wprintf_s_nid_postfix(u"%n", &widePosition) == -1 && PrintWideSecureOutput(static_cast(nullptr)) == -1, "wide standard output violations"); + + FileStream* sizedInput = SeededStream("17 hello Q 9 12abc key=val abcdef 4.5"); + char sizedWord[6]{}; + char sizedChar = 0; + Require(fscanf_s_nid_postfix(sizedInput, "%d %s %c", &number, sizedWord, 6u, &sizedChar, 1u) == 3, "fscanf_s assigns the sized conversions"); + Require(number == 17 && std::strcmp(sizedWord, "hello") == 0 && sizedChar == 'Q', "fscanf_s values"); + Require(ScanSecureStream(sizedInput, "%d", &number) == 1 && number == 9, "vfscanf_s continues from the stream position"); + Require(fscanf_s_nid_postfix(sizedInput, "%d", &number) == 1 && number == 12 && NextByte(sizedInput) == 'a', "fscanf_s leaves the character that ended the number"); + char key[4]{}; + char value[8]{}; + Require(fscanf_s_nid_postfix(sizedInput, "bc %3[a-z]=%7s", key, 4u, value, 8u) == 2, "fscanf_s with a set and a string"); + Require(std::strcmp(key, "key") == 0 && std::strcmp(value, "val") == 0 && NextByte(sizedInput) == ' ', "fscanf_s set and string values"); + char tooSmall[4] = {'x', 'x', 'x', 'x'}; + char siblingSmall[4] = {'x', 'x', 'x', 'x'}; + Require(sscanf_s_nid_postfix("abcdef", "%s", siblingSmall, 4u) == 0 && siblingSmall[0] == '\0', "sscanf_s reference for an undersized buffer"); + Require(fscanf_s_nid_postfix(sizedInput, "%s", tooSmall, 4u) == 0 && tooSmall[0] == '\0', "fscanf_s rejects an undersized buffer like sscanf_s"); + sizedInput->Close(); + errno = 0; + Require(fscanf_s_nid_postfix(sizedInput, "%d", &number) == EOF && errno == 9, "fscanf_s on a closed stream sets EBADF"); + delete sizedInput; + + FileStream* tailInput = SeededStream("x 3.5 7"); + int count = 0; + Require(ScanSecureStream(tailInput, "%d", &number) == 0, "vfscanf_s matching failure"); + Require(fscanf_s_nid_postfix(tailInput, " x %*f%n %d", &count, &number) == 1 && count == 5 && number == 7, "fscanf_s suppression and %n"); + Require(fscanf_s_nid_postfix(tailInput, "%d", &number) == EOF, "fscanf_s at end of file"); + tailInput->Close(); + delete tailInput; + + FileStream* nullInput = SeededStream("5 6"); + Require(fscanf_s_nid_postfix(nullInput, "%d", static_cast(nullptr)) == EOF, "fscanf_s null destination"); + Require(fscanf_s_nid_postfix(nullInput, static_cast(nullptr)) == EOF, "fscanf_s null format"); + Require(fscanf_s_nid_postfix(nullptr, "%d", &number) == EOF && ScanSecureStream(nullptr, "%d", &number) == EOF, "fscanf_s null stream"); + nullInput->Close(); + delete nullInput; + + FileStream* const savedInput = __stdinp_nid_postfix; + FileStream* redirected = SeededStream("21 word 3 more"); + __stdinp_nid_postfix = redirected; + char redirectedWord[8]{}; + int redirectedNumber = 0; + Require(scanf_s_nid_postfix("%d %7s", &number, redirectedWord, 8u) == 2 && number == 21 && std::strcmp(redirectedWord, "word") == 0, "scanf_s reads the guest standard input"); + Require(ScanSecureInput("%d %4s", &redirectedNumber, redirectedWord, 5u) == 2 && redirectedNumber == 3 && std::strcmp(redirectedWord, "more") == 0, "vscanf_s reads the guest standard input"); + Require(scanf_s_nid_postfix("%d", &number) == EOF, "scanf_s at end of input"); + __stdinp_nid_postfix = savedInput; + redirected->Close(); + delete redirected; + std::int32_t a = 0, b = 0; Require(ScanWide(u" 31 -4", u"%d%d", &a, &b) == 2 && a == 31 && b == -4, "vswscanf"); Require(ScanWide(u"", u"%d", &a) == EOF, "vswscanf on empty input"); diff --git a/core/libs/tests/GuestStrftime.cpp b/core/libs/tests/GuestStrftime.cpp index f4a40f8712..cf18e7c687 100644 --- a/core/libs/tests/GuestStrftime.cpp +++ b/core/libs/tests/GuestStrftime.cpp @@ -4,88 +4,250 @@ #include #include #include +#include namespace { -GuestTm MakeTime(int year, int hour, int wday, int yday, int isdst) { +bool correct = true; + +GuestTm MakeTime() { GuestTm time{}; - time.tm_sec = 5; - time.tm_min = 4; - time.tm_hour = hour; + time.tm_year = 126; + time.tm_mon = 9; time.tm_mday = 2; - time.tm_mon = 0; - time.tm_year = year; - time.tm_wday = wday; - time.tm_yday = yday; - time.tm_isdst = isdst; + time.tm_hour = 13; + time.tm_min = 5; + time.tm_sec = 9; + time.tm_wday = 5; + time.tm_yday = 274; time.tm_gmtoff = 19800; - time.tm_zone = "IST"; + time.tm_zone = "XYZ"; return time; } +void Check(const char* name, const GuestTm& time, const char* format, const char* expected) { + char text[256]; + const std::size_t length = strftime_nid_postfix(text, sizeof(text), format, &time); + if (length != std::strlen(expected) || std::strcmp(text, expected) != 0) { + std::fprintf(stderr, "strftime %s (\"%s\"): expected \"%s\", received \"%s\" (%zu)\n", name, format, expected, length == 0 ? "" : text, length); + correct = false; + } +} + +void CheckAsctime(const char* name, const GuestTm& time, const char* expected) { + const char* result = asctime_nid_postfix(&time); + if (std::strcmp(result, expected) != 0) { + std::fprintf(stderr, "asctime %s: expected \"%s\", received \"%s\"\n", name, expected, result); + correct = false; + } +} + } int main() { - struct Case { - const char* name; - int year; - int hour; - int wday; - int yday; - int isdst; - const char* format; + if (setenv_nid_postfix("TZ", "UTC+3", 1) != 0) return 1; + tzset_nid_postfix(); + const GuestTm base = MakeTime(); + Check("names", base, "%a|%A|%b|%B|%h", "Fri|Friday|Oct|October|Oct"); + Check("composites", base, "%c|%D|%x|%X|%T|%r|%R|%F", "Fri Oct 2 13:05:09 2026|10/02/26|10/02/26|13:05:09|13:05:09|01:05:09 PM|13:05|2026-10-02"); + Check("numbers", base, "%C|%d|%e|%g|%G|%H|%I|%j|%m|%M|%p|%S|%u|%U|%V|%w|%W|%y|%Y", "20|02| 2|26|2026|13|01|275|10|05|PM|09|5|39|40|5|39|26|2026"); + Check("whitespace and percent", base, "%n%t%%", "\n\t%"); + Check("alternative forms", base, "%Ec|%EC|%Ex|%EX|%Ey|%EY|%Od|%Oe|%OH|%OI|%Om|%OM|%OS|%Ou|%OU|%OV|%Ow|%OW|%Oy|%Ez", "Fri Oct 2 13:05:09 2026|20|10/02/26|13:05:09|26|2026|02| 2|13|01|10|05|09|5|39|40|5|39|26|-0300"); + Check("other conversions", base, "%k|%l|%P|%s|%+|%v|%Q|%q", "%k|%l|%P|%s|%+|%v|%Q|%q"); + Check("flags and widths", base, "%-d|%_d|%0e|%^a|%#b|%10Y", "%-d|%_d|%0e|%^a|%#b|%10Y"); + Check("modifier before an unknown conversion", base, "%Ea|%Oa|%EE|%OO|%Ek|%EY", "Fri|Fri|%E|%O|%k|2026"); + Check("trailing percent", base, "ab%", "ab%"); + Check("zone", base, "%z|%Z", "-0300|UTC"); + + GuestTm zone = base; + zone.tm_isdst = 1; + zone.tm_gmtoff = -1; + Check("zone in daylight time", zone, "%z", "-0200"); + zone.tm_isdst = -1; + Check("zone with unknown daylight time", zone, "%z", "-0300"); + + for (int value = -3; value <= 14; ++value) { + GuestTm time = base; + time.tm_wday = value; + time.tm_mon = value; + static const char* const expected[] = { + "Sun|Sun|Jan|Jan|00|0|0", "Sun|Sun|Jan|Jan|00|0|0", "Sun|Sun|Jan|Jan|00|0|0", "Sun|Sunday|Jan|January|01|7|0", + "Mon|Monday|Feb|February|02|1|1", "Tue|Tuesday|Mar|March|03|2|2", "Wed|Wednesday|Apr|April|04|3|3", "Thu|Thursday|May|May|05|4|4", + "Fri|Friday|Jun|June|06|5|5", "Sat|Saturday|Jul|July|07|6|6", "||Aug|August|08|7|7", "||Sep|September|09|8|8", + "||Oct|October|10|9|9", "||Nov|November|11|0|0", "||Dec|December|12|1|1", "||||13|2|2", "||||14|3|3", "||||15|4|4", + }; + char name[24]; + std::snprintf(name, sizeof(name), "index %d", value); + Check(name, time, "%a|%A|%b|%B|%m|%u|%w", expected[value + 3]); + } + + struct Field { + int value; const char* expected; }; - constexpr Case cases[] = { - {"alternative", 126, 3, 5, 1, 0, "%Ea %OX %OD %EF %EEY", "Fri 03:04:05 01/02/26 2026-01-02 EY"}, - {"padding", 126, 3, 5, 1, 0, "%-d|%_d|%0e|%-k|%_+|%--d", "2| 2|02|3|Fri Jan 2 03:04:05 IST 2026|-d"}, - {"zone", 126, 3, 5, 1, 0, "%z %Z %0z", "+0530 IST +0530"}, - {"year 899", -1001, 3, 5, 1, 0, "%Y %C %y %G %g %F", "0899 08 99 0899 99 0899-01-02"}, - {"year -1", -1901, 3, 5, 1, 0, "%Y %C %y %c", "-001 -0 01 Fri Jan 2 03:04:05 -001"}, - {"year 12345", 10445, 3, 5, 1, 0, "%Y %C %y %D %v", "12345 123 45 01/02/45 2-Jan-12345"}, - {"out of range", 126, 100, 8, 400, 0, "%I %l %u %W %U %j %a %p", "04 4 8 57 57 401 ? PM"}, - {"unknown dst", 126, 3, 5, 1, -1, "[%z][%Z]", "[][IST]"}, - {"literals", 126, 3, 5, 1, 0, "%q %% %", "q % %"}, + constexpr Field fields[] = { + {-25, "00|00|AM|00|00|00| 0|000|00|00"}, {-13, "00|00|AM|00|00|00| 0|000|00|00"}, {-12, "00|00|AM|00|00|00| 0|000|00|00"}, + {-1, "00|11|AM|00|00|00| 0|000|00|00"}, {0, "00|12|AM|00|00|00| 0|001|00|00"}, {1, "01|01|AM|01|01|01| 1|002|00|00"}, + {11, "11|11|AM|11|11|11|11|012|01|02"}, {12, "12|12|PM|12|12|12|12|013|02|02"}, {13, "13|01|PM|13|13|13|13|014|02|02"}, + {25, "25|01|PM|25|25|25|25|026|03|04"}, {99, "99|03|PM|99|99|99|99|100|14|14"}, {100, "00|04|PM|00|00|00| 0|101|14|14"}, }; - bool correct = true; - for (const auto& test : cases) { - const GuestTm time = MakeTime(test.year, test.hour, test.wday, test.yday, test.isdst); - char text[128]; - const std::size_t length = strftime_nid_postfix(text, sizeof(text), test.format, &time); - if (length != std::strlen(test.expected) || std::strcmp(text, test.expected) != 0) { - std::fprintf(stderr, "strftime %s (\"%s\"): expected \"%s\", received \"%s\" (%zu)\n", - test.name, test.format, test.expected, length == 0 ? "" : text, length); - correct = false; - } + for (const auto& field : fields) { + GuestTm time = base; + time.tm_hour = field.value; + time.tm_min = field.value; + time.tm_sec = field.value; + time.tm_mday = field.value; + time.tm_yday = field.value; + char name[24]; + std::snprintf(name, sizeof(name), "field %d", field.value); + Check(name, time, "%H|%I|%p|%M|%S|%d|%e|%j|%U|%W", field.expected); } - const GuestTm now = MakeTime(126, 3, 5, 1, 0); - char text[32]; - if (strftime_nid_postfix(text, 10, "%Y-%m-%d", &now) != 0 || strftime_nid_postfix(text, 11, "%Y-%m-%d", &now) != 10) { - std::fprintf(stderr, "strftime does not return 0 when the result and its terminator do not fit\n"); - correct = false; + + constexpr Field numbers[] = { + {-10, "00| 0|000|00|00|00"}, {9, "09| 9|010|09|09|09"}, {101, "01| 1|102|01|01|01"}, {999, "99|99|000|99|99|99"}, + {1000, "00| 0|001|00|00|00"}, {123456, "56|56|457|56|56|56"}, + }; + for (const auto& field : numbers) { + GuestTm time = base; + time.tm_hour = field.value; + time.tm_min = field.value; + time.tm_sec = field.value; + time.tm_mday = field.value; + time.tm_yday = field.value; + char name[24]; + std::snprintf(name, sizeof(name), "number %d", field.value); + Check(name, time, "%d|%e|%j|%H|%M|%S", field.expected); } - if (strftime_nid_postfix(text, sizeof(text), nullptr, &now) == 0 || std::strcmp(text, "Fri Jan 2 03:04:05 2026") != 0) { - std::fprintf(stderr, "strftime with a null format does not format %%c\n"); - correct = false; - } - struct AsctimeCase { - int year; + GuestTm leap = base; + leap.tm_sec = 61; + Check("leap second", leap, "%S|%T", "61|13:05:61"); + + struct Year { + int tmYear; const char* expected; }; - constexpr AsctimeCase asctimeCases[] = { - {126, "Fri Jan 2 03:04:05 2026\n"}, - {-1001, "Fri Jan 2 03:04:05 0899\n"}, - {-1901, "Fri Jan 2 03:04:05 -001\n"}, - {10445, "Fri Jan 2 03:04:05 12345\n"}, - {-2147483647 - 1, "Fri Jan 2 03:04:05 -2147481748\n"}, + constexpr Year years[] = { + {-14245, "0000|00|55|0000|55"}, {-11900, "0000|00|00|0000|00"}, {-2001, "0000|00|99|0000|99"}, {-1999, "0000|00|01|0000|01"}, + {-1901, "0000|00|99|0000|99"}, {-1900, "0000|00|00|0000|00"}, {-1899, "0001|01|01|0001|01"}, {-1800, "0100|01|00|0100|00"}, + {-1, "1899|19|99|1899|99"}, {0, "1900|19|00|1900|00"}, {8099, "9999|99|99|9999|99"}, {8100, "0000|00|00|0000|00"}, + {8101, "0001|00|01|0001|01"}, {10099, "1999|19|99|1999|99"}, {10445, "2345|23|45|2345|45"}, }; - for (const auto& test : asctimeCases) { - const GuestTm time = MakeTime(test.year, 3, 5, 1, 0); - const char* result = asctime_nid_postfix(&time); - if (std::strcmp(result, test.expected) != 0) { - std::fprintf(stderr, "asctime of tm_year %d: expected \"%s\", received \"%s\"\n", test.year, test.expected, result); - correct = false; - } + for (const auto& year : years) { + GuestTm time = base; + time.tm_year = year.tmYear; + char name[24]; + std::snprintf(name, sizeof(name), "year %d", year.tmYear); + Check(name, time, "%Y|%C|%y|%G|%g", year.expected); } + + struct Iso { + int year, mon, mday, wday, yday; + const char* expected; + }; + constexpr Iso weeks[] = { + {126, 0, 1, 4, 0, "2026|26|01|00|00"}, {126, 0, 4, 0, 3, "2026|26|01|01|00"}, {126, 0, 5, 1, 4, "2026|26|02|01|01"}, + {120, 11, 31, 4, 365, "2020|20|53|52|52"}, {121, 0, 3, 0, 2, "2020|20|53|01|00"}, {121, 0, 4, 1, 3, "2021|21|01|01|01"}, + {124, 11, 30, 1, 364, "2025|25|01|52|53"}, {104, 11, 31, 5, 365, "2004|04|53|52|52"}, {105, 0, 2, 0, 1, "2004|04|53|01|00"}, + {126, 0, 1, 9, 10, "2026|26|02|02|02"}, {126, 0, 1, -2, 10, "2026|26|02|01|01"}, {-1901, 0, 1, 4, 0, "0000|99|01|00|00"}, + }; + for (const auto& week : weeks) { + GuestTm time = base; + time.tm_year = week.year; + time.tm_mon = week.mon; + time.tm_mday = week.mday; + time.tm_wday = week.wday; + time.tm_yday = week.yday; + char name[48]; + std::snprintf(name, sizeof(name), "week %d/%d wday %d yday %d", week.year, week.mon, week.wday, week.yday); + Check(name, time, "%G|%g|%V|%U|%W", week.expected); + } + + constexpr Iso outOfRangeWeeks[] = { + {126, 0, 11, -2, 10, "01|01"}, {126, 1, 5, 4, 400, "57|57"}, {126, 3, 11, -19, 100, "16|16"}, {126, 9, 5, -16, 277, "40|41"}, + {126, 0, 19, -12, 18, "04|04"}, {126, 0, 1, 6, 1365, "95|95"}, {97, 0, 1, 6, 415, "59|59"}, {113, 11, 31, 1, 365, "53|53"}, + {7, 11, 31, 1, 365, "53|53"}, {126, 9, 24, 6, 296, "42|42"}, + }; + for (const auto& week : outOfRangeWeeks) { + GuestTm time = base; + time.tm_year = week.year; + time.tm_mon = week.mon; + time.tm_mday = week.mday; + time.tm_wday = week.wday; + time.tm_yday = week.yday; + char name[48]; + std::snprintf(name, sizeof(name), "week number wday %d yday %d", week.wday, week.yday); + Check(name, time, "%U|%W", week.expected); + } + + char text[8]; + std::memset(text, '#', sizeof(text)); + if (strftime_nid_postfix(text, 5, "%Y", &base) != 4 || std::strcmp(text, "2026") != 0) { + std::fprintf(stderr, "strftime does not fill a buffer that holds the result and its terminator\n"); + correct = false; + } + std::memset(text, '#', sizeof(text)); + if (strftime_nid_postfix(text, 4, "%Y", &base) != 0 || std::memcmp(text, "2026#", 5) != 0) { + std::fprintf(stderr, "strftime with no room for the terminator does not return 0 after writing the conversion\n"); + correct = false; + } + std::memset(text, '#', sizeof(text)); + if (strftime_nid_postfix(text, 5, "ab%Ycd", &base) != 0 || std::memcmp(text, "ab####", 6) != 0) { + std::fprintf(stderr, "strftime writes part of a conversion that does not fit\n"); + correct = false; + } + std::memset(text, '#', sizeof(text)); + if (strftime_nid_postfix(text, 8, "%t%D", &base) != 0 || std::memcmp(text, "\t10/02/#", 8) != 0) { + std::fprintf(stderr, "strftime does not write the parts of a composite conversion that fit\n"); + correct = false; + } + std::memset(text, '#', sizeof(text)); + if (strftime_nid_postfix(text, 2, "%X%#H", &base) != 0 || std::memcmp(text, "13#", 3) != 0) { + std::fprintf(stderr, "strftime does not stop a composite conversion at the first part that does not fit\n"); + correct = false; + } + std::memset(text, '#', sizeof(text)); + if (strftime_nid_postfix(text, 3, "abc", &base) != 0 || std::memcmp(text, "abc#", 4) != 0) { + std::fprintf(stderr, "strftime does not write the literals that fit\n"); + correct = false; + } + std::memset(text, '#', sizeof(text)); + if (strftime_nid_postfix(text, 0, "%Y", &base) != 0 || text[0] != '#') { + std::fprintf(stderr, "strftime writes to a zero-size buffer\n"); + correct = false; + } + bool threw = false; + try { + strftime_nid_postfix(text, sizeof(text), "%E", &base); + } catch (const std::invalid_argument&) { + threw = true; + } + if (!threw) { + std::fprintf(stderr, "strftime does not throw for a format ending after a modifier\n"); + correct = false; + } + + CheckAsctime("base", base, "Fri Oct 2 13:05:09 2026\n"); + GuestTm negative = base; + negative.tm_wday = -1; + negative.tm_mon = -1; + CheckAsctime("negative names", negative, "Sun Jan 2 13:05:09 2026\n"); + GuestTm past = base; + past.tm_wday = 7; + past.tm_mon = 12; + CheckAsctime("names past the tables", past, " 2 13:05:09 2026\n"); + GuestTm big = base; + big.tm_mday = 40; + big.tm_hour = 25; + big.tm_min = 61; + big.tm_sec = 61; + CheckAsctime("large fields", big, "Fri Oct 40 25:61:61 2026\n"); + big.tm_mday = 123456; + big.tm_hour = -10; + CheckAsctime("wrapped and negative fields", big, "Fri Oct 56 00:61:61 2026\n"); + GuestTm year = base; + year.tm_year = 8100; + CheckAsctime("year 10000", year, "Fri Oct 2 13:05:09 0000\n"); + year.tm_year = -1901; + CheckAsctime("year -1", year, "Fri Oct 2 13:05:09 0000\n"); return correct ? 0 : 1; } diff --git a/core/libs/tests/GuestStringCompare.cpp b/core/libs/tests/GuestStringCompare.cpp new file mode 100644 index 0000000000..9fc7691c39 --- /dev/null +++ b/core/libs/tests/GuestStringCompare.cpp @@ -0,0 +1,44 @@ +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +namespace { + +void Require(bool value) { + if (!value) std::abort(); +} + +} + +int main() { + Require(strcmp_nid_postfix("abc", "abc") == 0); + Require(strcmp_nid_postfix("abc", "abd") == -1); + Require(strcmp_nid_postfix("abd", "abc") == 1); + Require(strcmp_nid_postfix("a", "z") == -25); + Require(strcmp_nid_postfix("abc", "abcd") == -100); + Require(strcmp_nid_postfix("abcd", "abc") == 100); + Require(strcmp_nid_postfix("\x80", "a") == 31); + Require(strcmp_nid_postfix("", "") == 0); + + Require(strncmp_nid_postfix("abcdef", "abcxyz", 3) == 0); + Require(strncmp_nid_postfix("abcdef", "abcxyz", 4) == -20); + Require(strncmp_nid_postfix("abc", "xyz", 0) == 0); + Require(strncmp_nid_postfix("abc\0x", "abc\0y", 6) == 0); + Require(strncmp_nid_postfix("ab", "abc", 8) == -99); + + const unsigned char base[] = {0x01, 0x02, 0x03}; + const unsigned char higher[] = {0x01, 0x02, 0x04}; + const unsigned char top[] = {0x01, 0x02, 0xff}; + const unsigned char low[] = {0x01, 0x02, 0x01}; + Require(memcmp_nid_postfix(base, base, 3) == 0); + Require(memcmp_nid_postfix(base, higher, 3) == -1); + Require(memcmp_nid_postfix(top, low, 3) == 254); + Require(memcmp_nid_postfix(top, low, 2) == 0); + const unsigned char sign[] = {0x80}; + const unsigned char one[] = {0x01}; + Require(memcmp_nid_postfix(sign, one, 1) == 127); + const unsigned char withNul[] = {0x00, 0x05}; + const unsigned char withNulLow[] = {0x00, 0x02}; + Require(memcmp_nid_postfix(withNul, withNulLow, 2) == 3); +} diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index 8debeb251f..c7ee8ee32e 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -44,6 +44,40 @@ static bool CheckMemcpyOverlap() { return correct; } +static int Sign(int value) { + return value < 0 ? -1 : value > 0 ? 1 : 0; +} + +static void CheckStandardStrings() { + Require(strcmp_nid_postfix("abc", "abc") == 0 && Sign(strcmp_nid_postfix("abc", "abd")) == -1 && Sign(strcmp_nid_postfix("abc", "ab")) == 1); + Require(Sign(strcmp_nid_postfix("\x80", "\x7f")) == 1); + Require(strncmp_nid_postfix("abcX", "abcY", 3) == 0 && Sign(strncmp_nid_postfix("abcX", "abcY", 4)) == -1 && strncmp_nid_postfix("a", "b", 0) == 0); + Require(strcoll_nid_postfix("same", "same") == 0 && Sign(strcoll_nid_postfix("B", "a")) == -1); + Require(strlen_nid_postfix("") == 0 && strlen_nid_postfix("seven 7") == 7); + + char buffer[16]; + std::memset(buffer, 'x', sizeof(buffer)); + Require(strcpy_nid_postfix(buffer, "ab") == buffer && std::memcmp(buffer, "ab\0x", 4) == 0); + Require(strcat_nid_postfix(buffer, "cd") == buffer && std::strcmp(buffer, "abcd") == 0); + + std::memset(buffer, 'x', sizeof(buffer)); + Require(strncpy_nid_postfix(buffer, "ab", 5) == buffer && std::memcmp(buffer, "ab\0\0\0x", 6) == 0); + std::memset(buffer, 'x', sizeof(buffer)); + Require(strncpy_nid_postfix(buffer, "abcdef", 3) == buffer && std::memcmp(buffer, "abcx", 4) == 0); + + const char* text = "needle in a haystack"; + Require(strstr_nid_postfix(text, "hay") == text + 12 && strstr_nid_postfix(text, "") == text && strstr_nid_postfix(text, "hey") == nullptr); + Require(strspn_nid_postfix("aabbc", "ab") == 4 && strspn_nid_postfix("xyz", "ab") == 0 && strspn_nid_postfix("ab", "") == 0); + + unsigned char bytes[6] = {1, 2, 3, 4, 5, 6}; + 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); + bzero_nid_postfix(bytes + 1, 4); + Require(bytes[0] == 1 && bytes[1] == 0 && bytes[4] == 0 && bytes[5] == 9); +} + static void CheckSecureCopySizeLimits() { using Copy = int (APS5_VABI*)(void*, std::size_t, const void*, std::size_t); constexpr std::size_t RsizeMax = SIZE_MAX >> 1; @@ -142,6 +176,8 @@ static void CheckSecureStringSizeLimits() { } static void CheckCaseInsensitiveComparison() { + Require(strcoll_nid_postfix("a", "z") == -25 && strcoll_nid_postfix("z", "a") == 25 && strcoll_nid_postfix("ab", "abc") == -99); + Require(strcoll_nid_postfix("A", "a") == -32 && strcoll_nid_postfix("same", "same") == 0 && strcoll_nid_postfix("\xff", "\x01") == 254); Require(strcasecmp_nid_postfix("", "") == 0 && strcasecmp_nid_postfix("AbC", "aBc") == 0); Require(strcasecmp_nid_postfix("A", "b") == -1 && strcasecmp_nid_postfix("b", "A") == 1); Require(strcasecmp_nid_postfix("ab", "A") == 1 && strcasecmp_nid_postfix("AB", "a") == 1); @@ -297,6 +333,7 @@ static void CheckCharacterConversionInSearches() { } int main() { + CheckStandardStrings(); CheckStrtonum(); CheckBcopy(); if (!CheckDirnameLengths()) return 1; diff --git a/core/libs/tests/GuestSulphaConfig.cpp b/core/libs/tests/GuestSulphaConfig.cpp new file mode 100644 index 0000000000..007930e68a --- /dev/null +++ b/core/libs/tests/GuestSulphaConfig.cpp @@ -0,0 +1,46 @@ +#include "prx/libSceSulphaDrv/libSceSulphaDrv.hpp" +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +int main() { + errno = 7; + Require(sceSulphaGetDefaultConfig(nullptr) == static_cast(0x804d0003u) && errno == 7, + "A null configuration must return the native argument error without changing errno"); + struct Config { + std::uint64_t size; + std::uint64_t capacity; + std::uint32_t setting; + std::uint32_t port; + }; + static_assert(sizeof(Config) == 24); + for (const auto shift : {0u, 1u}) for (const auto initial : std::array{0, 24, 560, UINT64_MAX}) { + for (const unsigned char pattern : {0xa5, 0x5a}) { + std::array storage; + storage.fill(pattern); + auto* config = storage.data() + 8 + shift; + std::memcpy(config, &initial, sizeof(initial)); + errno = 7; + Require(sceSulphaGetDefaultConfig(config) == 0 && errno == 7, + "Default configuration must accept non-null storage regardless of its incoming size"); + Config result; + std::memcpy(&result, config, sizeof(result)); + Require(result.size == 24 && result.capacity == 4194304 && result.setting == 3 && result.port == 4001, + "Default configuration must match the native 24-byte record"); + for (std::size_t i = 0; i < storage.size(); ++i) { + if (i < 8 + shift || i >= 8 + shift + sizeof(Config)) + Require(storage[i] == pattern, "Default configuration must preserve bytes outside its record"); + } + } + } +} diff --git a/core/libs/tests/GuestSysmodule.cpp b/core/libs/tests/GuestSysmodule.cpp index b416232025..c4955a5348 100644 --- a/core/libs/tests/GuestSysmodule.cpp +++ b/core/libs/tests/GuestSysmodule.cpp @@ -117,12 +117,16 @@ void CheckUnknownIds() { const std::uint16_t id = UnknownModuleId(); const std::uint32_t internalId = id | kInternalFlag; Require(kModuleTable.find(internalId) == kModuleTable.end()); - Require(sceSysmoduleIsLoaded(id) == 0); - Require(sceSysmoduleLoadModule(id) == 0); - Require(sceSysmoduleIsLoaded(id) == 0); - Require(sceSysmoduleUnloadModule(id) == 0); - Require(sceSysmoduleUnloadModule(id) == 0); - Require(sceSysmoduleIsLoaded(id) == 0); + for (const std::uint16_t rejected : {id, std::uint16_t{0x0001}, std::uint16_t{0x0080}, std::uint16_t{0x00c5}, std::uint16_t{0x0100}, std::uint16_t{0x8000}}) { + Require(sceSysmoduleIsLoaded(rejected) == kInvalidValue); + Require(sceSysmoduleLoadModule(rejected) == kInvalidValue); + Require(sceSysmoduleIsLoaded(rejected) == kInvalidValue); + Require(sceSysmoduleUnloadModule(rejected) == kInvalidValue); + } + Require(kModuleTable.find(0x0111) == kModuleTable.end()); + Require(sceSysmoduleIsLoaded(0x0111) == 0); + Require(sceSysmoduleLoadModule(0x0111) == 0); + Require(sceSysmoduleUnloadModule(0x0111) == 0); Require(ThrowsRuntimeError([internalId] { sceSysmoduleLoadModuleInternal(internalId); })); Require(ThrowsRuntimeError([internalId] { sceSysmoduleUnloadModuleInternal(internalId); })); int result = 0x5a5a5a5a; diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index ec022beb6f..db613214fb 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include static_assert(offsetof(SceKernelSystemSwVersion, size) == 0x00); @@ -96,7 +97,6 @@ static void CheckPhysicalMemorySysctl(int number, const char* name) { Require(sysctl_nid_postfix(mib, 2, &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); Require(sysctlbyname_nid_postfix(name, &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); } -#if defined(__x86_64__) || defined(__i386__) static void CheckModelSysctl() { const int mib[] = {6, 2}; *__error_nid_postfix() = 13; @@ -115,7 +115,6 @@ static void CheckModelSysctl() { length = sizeof(model); Require(sysctlbyname_nid_postfix("hw.model", model, &length, model, size) == -1 && *__error_nid_postfix() == 1); } -#endif static void CheckTscFrequencySysctl() { *__error_nid_postfix() = 13; std::size_t length = 0; @@ -149,9 +148,13 @@ static void CheckUnansweredSysctl() { static constexpr const char* names[] = { "hw.byteorder", "hw.cacheline_size", + "hw.floatingpoint", "hw.machine_arch", "hw.usermem", + "kern.bootfile", "kern.clockrate", + "kern.cp_time", + "kern.ident", "kern.maxfiles", "kern.maxfilesperproc", "kern.maxproc", @@ -311,9 +314,10 @@ struct SystemString { }; static const SystemString SystemStrings[] = { {{1, 1}, "kern.ostype", "FreeBSD"}, - {{1, 2}, "kern.osrelease", "11.4-RELEASE"}, + {{1, 2}, "kern.osrelease", "0.0-prototype"}, {{1, 4}, "kern.version", "FreeBSD 11.4-RELEASE\n"}, - {{1, 10}, "kern.hostname", "AnyPS5"}, + {{1, 10}, "kern.hostname", ""}, + {{6, 2}, "hw.model", "100-000001020 "}, {{6, 1}, "hw.machine", "amd64"}, }; static void CheckSystemStringSysctl() { @@ -322,16 +326,16 @@ static void CheckSystemStringSysctl() { *__error_nid_postfix() = 13; std::size_t length = 0; Require(sysctl_nid_postfix(entry.mib, 2, nullptr, &length, nullptr, 0) == 0 && length == size); - char buffer[32]; + char buffer[64]; std::memset(buffer, 0xaa, sizeof(buffer)); length = sizeof(buffer); Require(sysctl_nid_postfix(entry.mib, 2, buffer, &length, nullptr, 0) == 0 && length == size); Require(std::strcmp(buffer, entry.value) == 0 && buffer[size] == static_cast(0xaa)); Require(*__error_nid_postfix() == 13); std::memset(buffer, 0xaa, sizeof(buffer)); - length = 2; + length = size - 1; Require(sysctl_nid_postfix(entry.mib, 2, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); - Require(length == 2 && std::memcmp(buffer, entry.value, 2) == 0 && buffer[2] == static_cast(0xaa)); + Require(length == size - 1 && std::memcmp(buffer, entry.value, size - 1) == 0 && buffer[size - 1] == static_cast(0xaa)); length = sizeof(buffer); Require(sysctl_nid_postfix(entry.mib, 2, buffer, &length, "Prospero", sizeof("Prospero")) == -1 && *__error_nid_postfix() == 1); const int deeper[] = {entry.mib[0], entry.mib[1], 0}; @@ -343,7 +347,7 @@ static void CheckSystemStringSysctl() { static void CheckSystemStringNames() { for (const auto& entry : SystemStrings) { *__error_nid_postfix() = 13; - char buffer[32]; + char buffer[64]; std::memset(buffer, 0xaa, sizeof(buffer)); std::size_t length = sizeof(buffer); Require(sysctlbyname_nid_postfix(entry.name, buffer, &length, nullptr, 0) == 0 && length == std::strlen(entry.value) + 1); @@ -355,9 +359,34 @@ static void CheckSystemStringNames() { Require(mib[0] == entry.mib[0] && mib[1] == entry.mib[1] && mib[2] == 0x5a5a5a5a); } } +static void CheckMeasuredIntegerSysctl() { + struct MeasuredInteger { + int mib[2]; + const char* name; + std::int32_t value; + }; + static constexpr MeasuredInteger integers[] = { + {{1, 3}, "kern.osrevision", 199506}, + {{1, 8}, "kern.argmax", 262144}, + {{1, 18}, "kern.ngroups", 1023}, + }; + for (const auto& entry : integers) { + std::int32_t value = 0; + std::size_t length = sizeof(value); + *__error_nid_postfix() = 13; + Require(sysctl_nid_postfix(entry.mib, 2, &value, &length, nullptr, 0) == 0 && value == entry.value && length == sizeof(value)); + value = 0; + length = sizeof(value); + Require(sysctlbyname_nid_postfix(entry.name, &value, &length, nullptr, 0) == 0 && value == entry.value && *__error_nid_postfix() == 13); + length = 3; + Require(sysctlbyname_nid_postfix(entry.name, &value, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12 && length == 3); + length = sizeof(value); + Require(sysctl_nid_postfix(entry.mib, 2, &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); + } +} static void CheckOsReleaseDateSysctl() { const int mib[] = {1, 24}; - constexpr std::int32_t expected = 1104001; + constexpr std::int32_t expected = 900000; *__error_nid_postfix() = 13; std::size_t length = 0; Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == sizeof(expected)); @@ -887,14 +916,61 @@ static void CheckNameToMib() { *__error_nid_postfix() = 13; Require(sysctlnametomib_nid_postfix("kern.anyps5_no_such_name", mib, &size) == -1 && *__error_nid_postfix() == 2); } +static void CheckUnknownRootsAndTopLevelNames() { + for (const int root : {99, 10, 255, -1, -5}) { + const int mib[] = {root, 1}; + int value = 0x5a5a5a5a; + std::size_t length = sizeof(value); + *__error_nid_postfix() = 13; + Require(sysctl_nid_postfix(mib, 2, &value, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + Require(value == 0x5a5a5a5a && length == sizeof(value)); + } + struct TopLevel { + const char* name; + int number; + }; + for (const TopLevel& node : {TopLevel{"sysctl", 0}, TopLevel{"kern", 1}, TopLevel{"vm", 2}, TopLevel{"vfs", 3}, TopLevel{"net", 4}, + TopLevel{"debug", 5}, TopLevel{"hw", 6}, TopLevel{"machdep", 7}, TopLevel{"user", 8}, TopLevel{"p1003_1b", 9}}) { + for (const bool dotted : {false, true}) { + const std::string name = std::string(node.name) + (dotted ? "." : ""); + int mib[3] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + std::size_t size = 3; + Require(sysctlnametomib_nid_postfix(name.c_str(), mib, &size) == 0 && size == 1 && mib[0] == node.number && mib[1] == 0x5a5a5a5a); + int value = 0x5a5a5a5a; + std::size_t length = sizeof(value); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(name.c_str(), &value, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); + Require(value == 0x5a5a5a5a && length == sizeof(value)); + } + } + int mib[3] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + std::size_t size = 3; + Require(sysctlnametomib_nid_postfix("hw.ncpu.", mib, &size) == 0 && size == 2 && mib[0] == 6 && mib[1] == 3); + for (const char* name : {"kern..", ".", "kern.anyps5.x", "kernel"}) { + size = 3; + *__error_nid_postfix() = 13; + Require(sysctlnametomib_nid_postfix(name, mib, &size) == -1 && *__error_nid_postfix() == 2); + } + int value = 0; + std::size_t length = sizeof(value); + Require(sysctlbyname_nid_postfix("hw.ncpu.", &value, &length, nullptr, 0) == 0 && value > 0 && length == sizeof(value)); + std::string tooLong(1024, 'a'); + length = sizeof(value); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(tooLong.c_str(), &value, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 63); + tooLong.resize(1023); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(tooLong.c_str(), &value, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2); +} + int main() { + CheckUnknownRootsAndTopLevelNames(); CheckProcessorCountSysctl(); CheckPageSizeSysctl(); CheckPhysicalMemorySysctl(5, "hw.physmem"); CheckPhysicalMemorySysctl(12, "hw.realmem"); -#if defined(__x86_64__) || defined(__i386__) CheckModelSysctl(); -#endif + CheckMeasuredIntegerSysctl(); CheckTscFrequencySysctl(); CheckUnansweredSysctl(); CheckUnknownSysctlName(); diff --git a/core/libs/tests/GuestSystemFont.cpp b/core/libs/tests/GuestSystemFont.cpp index 5195bee16c..643b4761e0 100644 --- a/core/libs/tests/GuestSystemFont.cpp +++ b/core/libs/tests/GuestSystemFont.cpp @@ -343,6 +343,20 @@ int main() { Require(sceFontUnbindRenderer(mixed) == SCE_FONT_OK); Require(sceFontDestroyRenderer(&renderer) == SCE_FONT_OK); + const auto hangulFonts = root / "hangul-fonts"; + std::filesystem::create_directories(hangulFonts); + for (const auto* name : {"SST-Light.otf", "SST-Roman.otf", "SST-Bold.otf", "SSTJpPro-Regular.otf"}) { + WriteFile(hangulFonts / name, SquareGlyphFont('A', 600)); + } + WriteFile(hangulFonts / "SCEPS4Yoongd-Light.otf", SquareGlyphFont(0xAC00, 400)); + WriteFile(hangulFonts / "SCEPS4Yoongd-Medium.otf", SquareGlyphFont(0xAC00, 650)); + WriteFile(hangulFonts / "SCEPS4Yoongd-Bold.otf", SquareGlyphFont(0xAC00, 800)); + SetFontDirectory(hangulFonts); + Require(SetAdvanceOf(library, 0x1A072543u, 0xAC00) == 40.0f); + Require(SetAdvanceOf(library, 0x1A082543u, 0xAC00) == 40.0f); + Require(SetAdvanceOf(library, 0x18072444u, 0xAC00) == 65.0f); + Require(SetAdvanceOf(library, 0x18072447u, 0xAC00) == 80.0f); + for (FontHandle handle : {font, instance, substitute, memoryFont, mixed, mixedInstance}) Require(sceFontCloseFont(handle) == SCE_FONT_OK); Require(sceFontDestroyLibrary(&library) == SCE_FONT_OK); Require(allocations == 0); diff --git a/core/libs/tests/GuestSystemServiceLoadExecStandardError.cpp b/core/libs/tests/GuestSystemServiceLoadExecStandardError.cpp index 777c63ba32..f054b5793b 100644 --- a/core/libs/tests/GuestSystemServiceLoadExecStandardError.cpp +++ b/core/libs/tests/GuestSystemServiceLoadExecStandardError.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -25,6 +26,17 @@ std::string Drain(int descriptor) { return text; } +bool HostOutputIs(const std::string& text, const std::string& line) { + constexpr std::string_view WriteWatchUnavailable = "[memory] write watch unavailable: "; + std::size_t begin = 0; + if (text.compare(0, WriteWatchUnavailable.size(), WriteWatchUnavailable) == 0) { + const std::size_t end = text.find('\n'); + if (end == std::string::npos) return false; + begin = end + 1; + } + return text.compare(begin, std::string::npos, line) == 0; +} + void Run(const char* mode, const char* const* arguments) { int host[2] = {-1, -1}; int guest[2] = {-1, -1}; @@ -50,7 +62,7 @@ void Run(const char* mode, const char* const* arguments) { int status = 0; Require(::waitpid(child, &status, 0) == child); Require(WIFEXITED(status) && WEXITSTATUS(status) == 0); - Require(hostText == std::string("started ") + mode + "\n"); + Require(HostOutputIs(hostText, std::string("started ") + mode + "\n")); Require(guestText.empty()); } diff --git a/core/libs/tests/GuestThermalSensors.cpp b/core/libs/tests/GuestThermalSensors.cpp index 2fcdb8a5a7..de58b80a6a 100644 --- a/core/libs/tests/GuestThermalSensors.cpp +++ b/core/libs/tests/GuestThermalSensors.cpp @@ -1,7 +1,9 @@ #include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/KernelErrors.hpp" #include +#include #include #include #include @@ -61,14 +63,22 @@ int main() { Require(fallback[0] == 55 && fallback[1] == -1 && fallback[2] == -1); std::filesystem::remove_all(skipped); - int value = 0; - Require(sceKernelGetCpuTemperature(nullptr) == SCE_KERNEL_ERROR_EFAULT); - Require(sceKernelGetSocSensorTemperature(0, nullptr) == SCE_KERNEL_ERROR_EFAULT); - Require(sceKernelGetSocSensorTemperature(-1, &value) == SCE_KERNEL_ERROR_EINVAL); - Require(sceKernelGetSocSensorTemperature(3, &value) == SCE_KERNEL_ERROR_EINVAL); + *__error_nid_postfix() = 0; + Require(sceKernelGetCpuTemperature(nullptr) == SCE_KERNEL_ERROR_EFAULT && *__error_nid_postfix() == 14); + *__error_nid_postfix() = 0; + Require(sceKernelGetSocSensorTemperature(0, nullptr) == SCE_KERNEL_ERROR_EFAULT && *__error_nid_postfix() == 14); + for (const int invalid : {-1, 65, 256, INT_MAX, INT_MIN}) { + int untouched = -12345; + *__error_nid_postfix() = 0; + Require(sceKernelGetSocSensorTemperature(invalid, &untouched) == SCE_KERNEL_ERROR_EINVAL && *__error_nid_postfix() == 22 && untouched == -12345); + *__error_nid_postfix() = 0; + Require(sceKernelGetSocSensorTemperature(invalid, nullptr) == SCE_KERNEL_ERROR_EINVAL && *__error_nid_postfix() == 22); + } + for (const int unmapped : {3, 15, 32, 64}) { + *__error_nid_postfix() = 0; + Require(sceKernelGetSocSensorTemperature(unmapped, nullptr) == SCE_KERNEL_ERROR_EFAULT && *__error_nid_postfix() == 14); + } - int sentinel = -12345; - Require(sceKernelGetSocSensorTemperature(15, &sentinel) == SCE_KERNEL_ERROR_EINVAL && sentinel == -12345); int probe = -12345; const int result = sceKernelGetSocSensorTemperature(0, &probe); Require(result == 0 ? (probe >= 0 && probe <= 130) : (result == SCE_KERNEL_ERROR_EINVAL && probe == -12345)); diff --git a/core/libs/tests/GuestThreadCpuClock.cpp b/core/libs/tests/GuestThreadCpuClock.cpp index 5d5aabcff7..952c0652ea 100644 --- a/core/libs/tests/GuestThreadCpuClock.cpp +++ b/core/libs/tests/GuestThreadCpuClock.cpp @@ -7,12 +7,15 @@ #include #include #include +#include #include static constexpr int GUEST_EFAULT = 14; static constexpr int GUEST_EINVAL = 22; static constexpr int GUEST_CLOCK_MONOTONIC = 4; static constexpr int GUEST_CLOCK_THREAD_CPUTIME_ID = 14; +static constexpr int GUEST_CLOCK_PROCESS_CPUTIME_ID = 15; +static constexpr std::uint32_t PROCESS_CPU_CLOCK = 0xC0000000u; static constexpr std::int64_t NANOS_PER_SECOND = 1000000000LL; static constexpr std::int64_t NANOS_PER_MILLISECOND = 1000000LL; static constexpr std::int64_t BURN_NANOS = 200 * NANOS_PER_MILLISECOND; @@ -99,10 +102,36 @@ static void OtherThreadClockReadsThatThread() { Require(*__error_nid_postfix() == GUEST_EINVAL); } +static void ProcessClocksNameOnlyTheGuest() { + const int self = getpid_nid_postfix(); + const int own = static_cast(PROCESS_CPU_CLOCK | static_cast(self)); + const int other = static_cast(PROCESS_CPU_CLOCK | static_cast(self + 1)); + const std::int64_t start = Nanos(own); + BurnOwnCpu(BURN_NANOS); + const std::int64_t before = Nanos(own); + const std::int64_t reference = Nanos(GUEST_CLOCK_PROCESS_CPUTIME_ID); + const std::int64_t after = Nanos(own); + Require(before - start >= BURN_NANOS - TICK_MARGIN_NANOS); + Require(before <= reference + TICK_MARGIN_NANOS && reference <= after + TICK_MARGIN_NANOS); + KernelTimespec time{-1, -1}; + *__error_nid_postfix() = 0; + Require(clock_gettime_nid_postfix(other, &time) == -1 && *__error_nid_postfix() == GUEST_EINVAL); + Require(time.tv_sec == -1 && time.tv_nsec == -1); + Require(sceKernelClockGettime(other, &time) == SCE_KERNEL_ERROR_EINVAL); + KernelTimespec threadResolution{-1, -1}; + Require(clock_getres_nid_postfix(GUEST_CLOCK_THREAD_CPUTIME_ID, &threadResolution) == 0); + for (const int clockId : {own, other}) { + KernelTimespec resolution{-1, -1}; + Require(clock_getres_nid_postfix(clockId, &resolution) == 0); + Require(resolution.tv_sec == threadResolution.tv_sec && resolution.tv_nsec == threadResolution.tv_nsec); + } +} + int main() { int clockId = 0; Require(pthread_getcpuclockid_nid_postfix(nullptr, &clockId) == GUEST_EINVAL); Require(pthread_getcpuclockid_nid_postfix(pthread_self_nid_postfix(), nullptr) == GUEST_EFAULT); OwnClockFollowsThreadCpuTime(); OtherThreadClockReadsThatThread(); + ProcessClocksNameOnlyTheGuest(); } diff --git a/core/libs/tests/GuestTimeConversion.cpp b/core/libs/tests/GuestTimeConversion.cpp index fee547cf32..abc96e55f2 100644 --- a/core/libs/tests/GuestTimeConversion.cpp +++ b/core/libs/tests/GuestTimeConversion.cpp @@ -129,75 +129,41 @@ static void CheckBufferSize(const GuestTm& guest, const char* format, std::size_ Require(strftime_nid_postfix(buffer, 1, format, &guest) == 0, "strftime accepted a one byte buffer"); } -static const char* HostZoneName(int isdst) { -#ifdef _WIN32 - return _tzname[isdst != 0 ? 1 : 0]; -#else - return tzname[isdst != 0 ? 1 : 0]; -#endif -} -static void CheckFreeBsdConversions() { +static void CheckConsoleConversions() { const std::int64_t timer = 1700000000; GuestTm guest{}; Require(gmtime_s_nid_postfix(&timer, &guest) == &guest, "UTC conversion failed"); - CheckFormat(guest, "%k|%l", "22|10"); - CheckFormat(guest, "%v", "14-Nov-2023"); - CheckFormat(guest, "%+", "Tue Nov 14 22:13:20 UTC 2023"); - CheckFormat(guest, "%s", "1699992800"); + CheckFormat(guest, "%k|%l|%v|%+|%s", "%k|%l|%v|%+|%s"); CheckFormat(guest, "%%k %%l %%s %%v %%+", "%k %l %s %v %+"); - CheckFormat(guest, "%%%k", "%22"); - CheckFormat(guest, "[%k][%l]", "[22][10]"); - CheckFormat(guest, "%Ek|%Ol|%Os|%Ev|%O+", "22|10|1699992800|14-Nov-2023|Tue Nov 14 22:13:20 UTC 2023"); - CheckFormat(guest, "%E%k", "%k"); - CheckFormat(guest, "%O%l", "%l"); - CheckFormat(guest, "%E%s", "%s"); - CheckFormat(guest, "%E%v", "%v"); - CheckFormat(guest, "%O%+", "%+"); - CheckFormat(guest, "%k|%E%l|%l", "22|%l|10"); - CheckFormat(guest, "%E%", "%"); - CheckFormat(guest, "%O%%k", "%22"); - CheckFormat(guest, "%E%k%k", "%k22"); + CheckFormat(guest, "%Ek|%Ol|%Os|%Ev|%O+", "%k|%l|%s|%v|%+"); + CheckFormat(guest, "%Ec|%Ox|%EY|%Om", "Tue Nov 14 22:13:20 2023|11/14/23|2023|11"); + CheckFormat(guest, "%EE|%OO|%EO", "%E|%O|%O"); CheckBufferSize(guest, "%k", 2); - CheckBufferSize(guest, "%k:%l", 5); - CheckBufferSize(guest, "%+", 28); + CheckBufferSize(guest, "%c", 24); CheckBufferSize(guest, "%Ek", 2); + bool threw = false; + try { + char buffer[16]; + strftime_nid_postfix(buffer, sizeof(buffer), "ab%O", &guest); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw, "strftime accepted a format ending after a modifier"); guest.tm_zone = "XYZ"; - CheckFormat(guest, "%+", "Tue Nov 14 22:13:20 XYZ 2023"); - CheckFormat(guest, "%O+", "Tue Nov 14 22:13:20 XYZ 2023"); - guest.tm_zone = "A%B"; - CheckFormat(guest, "%+", "Tue Nov 14 22:13:20 A%B 2023"); - guest.tm_zone = nullptr; guest.tm_isdst = 0; - char expected[96]; - std::snprintf(expected, sizeof(expected), "Tue Nov 14 22:13:20 %s 2023", HostZoneName(0)); - CheckFormat(guest, "%+", expected); + CheckFormat(guest, "%Z", tzname_nid_postfix[0]); guest.tm_isdst = 1; - std::snprintf(expected, sizeof(expected), "Tue Nov 14 22:13:20 %s 2023", HostZoneName(1)); - CheckFormat(guest, "%+", expected); + CheckFormat(guest, "%Z", tzname_nid_postfix[1]); guest.tm_isdst = 0; - GuestTm local{}; - Require(localtime_s_nid_postfix(&timer, &local) == &local, "Local conversion failed"); - GuestTm normalized = local; - const std::int64_t mktimeResult = mktime_nid_postfix(&normalized); - Require(mktimeResult == timer, "mktime did not invert localtime"); - std::snprintf(expected, sizeof(expected), "%lld", static_cast(mktimeResult)); - CheckFormat(local, "%s", expected); - CheckFormat(local, "%Os", expected); - guest.tm_hour = 0; - CheckFormat(guest, "%k|%l", " 0|12"); + CheckFormat(guest, "%I|%p", "12|AM"); guest.tm_hour = 12; - CheckFormat(guest, "%k|%l", "12|12"); - guest.tm_hour = 5; - guest.tm_mday = 5; - CheckFormat(guest, "%k|%l|%v", " 5| 5| 5-Nov-2023"); + CheckFormat(guest, "%I|%p", "12|PM"); guest.tm_hour = 13; - CheckFormat(guest, "%k|%l", "13| 1"); - char tooSmall[4]{}; - Require(strftime_nid_postfix(tooSmall, sizeof(tooSmall), "%k:%l", &guest) == 0, "strftime accepted an undersized buffer"); + CheckFormat(guest, "%I|%p", "01|PM"); } static void CheckLiteralConversions() { @@ -209,31 +175,18 @@ static void CheckLiteralConversions() { const char* expected; }; constexpr FormatCase cases[]{ - {"%Q", "Q"}, - {"%P|%f|%N|%1|%#Y", "P|f|N|1|#Y"}, - {"[%Q] %Y-%m-%d", "[Q] 2023-11-14"}, - {"%EQ|%OQ|%_Q|%-Q|%0Q", "Q|Q|Q|Q|Q"}, - {"%E_Q|%_EQ|%O0Q|%-OQ", "Q|Q|Q|Q"}, - {"%EEY|%EOY|%OEY|%OOY", "EY|OY|EY|OY"}, - {"%EE%Y|%OO%k", "E2023|O22"}, - {"%--Y|%__Y|%00Y|%_0Y|%0-Y", "-Y|_Y|0Y|0Y|-Y"}, - {"%E_EY|%_EOY|%O0OY", "EY|OY|OY"}, + {"%Q", "%Q"}, + {"%P|%f|%N|%1|%#Y", "%P|%f|%N|%1|%#Y"}, + {"[%Q] %Y-%m-%d", "[%Q] 2023-11-14"}, + {"%EQ|%OQ|%_Q|%-Q|%0Q", "%Q|%Q|%_Q|%-Q|%0Q"}, + {"%--Y|%__Y|%00Y", "%--Y|%__Y|%00Y"}, {"%", "%"}, {"text%", "text%"}, - {"%E", "E"}, - {"%O", "O"}, - {"%_", "_"}, - {"%-", "-"}, - {"%0", "0"}, - {"%E_", "_"}, - {"%_E", "E"}, - {"%Q%", "Q%"}, - {"%k%", "22%"}, - {"%%Q|%%%Q|%%%%Q", "%Q|%Q|%%Q"}, - {"%E%Q|%O%Q|%_%Q|%0%Q|%-%Q", "%Q|%Q|%Q|%Q|%Q"}, - {"%E%%Q|%O%%%Q", "%Q|%%Q"}, - {"%Q|%Ek|%Ol|%Ev|%Y", "Q|22|10|14-Nov-2023|2023"}, - {"%\xc3\xa5", "\xc3\xa5"}, + {"%_", "%_"}, + {"%Q%", "%Q%"}, + {"%Y%", "2023%"}, + {"%%Q|%%%Q|%%%%Q", "%Q|%%Q|%%Q"}, + {"%\xc3\xa5", "%\xc3\xa5"}, {"%%", "%"}, {"literal text", "literal text"}, }; @@ -419,11 +372,10 @@ static void CheckGuestZone() { static bool ZoneFieldsConsistent(const GuestTm& time, std::int64_t instant, int hour) { const std::int64_t localSeconds = ((instant + time.tm_gmtoff) % 86400 + 86400) % 86400; if (time.tm_hour * 3600 + time.tm_min * 60 + time.tm_sec != localSeconds || time.tm_hour != hour) return false; - char expected[16]; - const std::int64_t offset = time.tm_gmtoff < 0 ? -time.tm_gmtoff : time.tm_gmtoff; - std::snprintf(expected, sizeof(expected), "%02d %c%04d", hour, time.tm_gmtoff < 0 ? '-' : '+', static_cast(offset / 3600 * 100)); - char formatted[16]{}; - return strftime_nid_postfix(formatted, sizeof(formatted), "%H %z", &time) == 8 && std::strcmp(formatted, expected) == 0; + char expected[4]; + std::snprintf(expected, sizeof(expected), "%02d", hour); + char formatted[4]{}; + return strftime_nid_postfix(formatted, sizeof(formatted), "%H", &time) == 2 && std::strcmp(formatted, expected) == 0; } static void CheckZoneRace() { @@ -566,7 +518,7 @@ int main() { Require(strftime_nid_postfix(formatted, sizeof(formatted), "%Y-%m-%d %H:%M:%S", &utc) == 19 && std::strcmp(formatted, "1970-01-01 00:00:00") == 0, "strftime did not read the guest tm"); - CheckFreeBsdConversions(); + CheckConsoleConversions(); CheckLocalRange(); CheckRegionZones(); #ifdef _WIN32 diff --git a/core/libs/tests/GuestTlsGetAddr.cpp b/core/libs/tests/GuestTlsGetAddr.cpp new file mode 100644 index 0000000000..e13ef11bde --- /dev/null +++ b/core/libs/tests/GuestTlsGetAddr.cpp @@ -0,0 +1,86 @@ +#include "prx/libkernel/include/Api.hpp" +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#if defined(_WIN32) +#include +#else +#include +#endif + +#if defined(_WIN32) +extern "C" ULONG _tls_index; + +namespace { + +using Cell = char; +constexpr Cell Initial = 0; + +KernelTlsIndex CellIndex() { return {reinterpret_cast(&_tls_index), 0}; } + +Cell* ThreadCell() { + auto* blocks = *reinterpret_cast(reinterpret_cast(NtCurrentTeb()) + 0x58); + return blocks[_tls_index]; +} + +} +#else +namespace { + +using Cell = std::uint32_t; +constexpr Cell Initial = 0x5ca1ab1e; +thread_local Cell Value = Initial; + +struct Module { + std::size_t id = 0; + void* data = nullptr; +}; + +Module MainModule() { + Module module; + dl_iterate_phdr([](dl_phdr_info* info, std::size_t, void* out) { + auto& found = *static_cast(out); + found = {info->dlpi_tls_modid, info->dlpi_tls_data}; + return 1; + }, &module); + return module; +} + +KernelTlsIndex CellIndex() { + const auto module = MainModule(); + return {module.id, static_cast(reinterpret_cast(&Value) - static_cast(module.data))}; +} + +Cell* ThreadCell() { return &Value; } + +} +#endif + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +} + +int main() { + const auto index = CellIndex(); + auto* own = static_cast(__tls_get_addr_nid_postfix(&index)); + Require(own == ThreadCell() && *own == Initial); + *own = 7; + Cell* other = nullptr; + Cell seen = 1; + std::thread([&] { + other = static_cast(__tls_get_addr_nid_postfix(&index)); + Require(other == ThreadCell()); + seen = *other; + *other = 9; + }).join(); + Require(other != own && seen == Initial && *own == 7); + *own = Initial; + bool threw = false; + try { __tls_get_addr_nid_postfix(nullptr); } catch (const std::invalid_argument&) { threw = true; } + Require(threw); +} diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index ac6d36fa4c..346f8c1a69 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -12,6 +12,34 @@ static void Require(bool value) { if (!value) std::abort(); } +static void CheckUnreachablePortIgnored() { + const int closing = socket_nid_postfix(2, 2, 0); + Require(closing >= 0); + std::array unreachable{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(closing, unreachable.data(), unreachable.size()) == 0); + std::uint32_t size = unreachable.size(); + Require(getsockname_nid_postfix(closing, unreachable.data(), &size) == 0 && size == unreachable.size()); + const int receiver = socket_nid_postfix(2, 2 | 0x20000000, 0); + const int sender = socket_nid_postfix(2, 2, 0); + Require(receiver >= 0 && sender >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(receiver, address.data(), address.size()) == 0); + size = address.size(); + Require(getsockname_nid_postfix(receiver, address.data(), &size) == 0); + Require(close_nid_postfix(closing) == 0); + Require(sendto_nid_postfix(receiver, "x", 1, 0, unreachable.data(), unreachable.size()) == 1); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + char received[4]{}; + for (int attempt = 0; attempt < 3; ++attempt) { + Require(recvfrom_nid_postfix(receiver, received, sizeof(received), 0, nullptr, nullptr) == -1 && *__error_nid_postfix() == 35); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + Require(sendto_nid_postfix(sender, "ok", 2, 0, address.data(), address.size()) == 2); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(recvfrom_nid_postfix(receiver, received, sizeof(received), 0, nullptr, nullptr) == 2 && received[0] == 'o'); + Require(close_nid_postfix(sender) == 0 && close_nid_postfix(receiver) == 0); +} + static void CheckAddressLengths(int family) { const std::uint32_t addressSize = family == 2 ? 16 : 28; const int receiver = socket_nid_postfix(family, 2, 0); @@ -310,4 +338,5 @@ int main() { Require(closeOnExec >= 0 && fcntl_nid_postfix(closeOnExec, 3) == 2); Require(close_nid_postfix(closeOnExec) == 0); Require(socket_nid_postfix(2, 0x20000000, 0) == -1 && *__error_nid_postfix() == 43); + CheckUnreachablePortIgnored(); } diff --git a/core/libs/tests/GuestUltSync.cpp b/core/libs/tests/GuestUltSync.cpp index a9d52c921a..f8e54452d0 100644 --- a/core/libs/tests/GuestUltSync.cpp +++ b/core/libs/tests/GuestUltSync.cpp @@ -1,4 +1,5 @@ #include "prx/libSceUlt/include/Api.hpp" +#include "prx/libkernel/include/Api.hpp" #include "SceTypes.hpp" #include #include @@ -7,6 +8,7 @@ #include #include #include +#include #include #include @@ -269,6 +271,406 @@ void Ulthreads() { CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == Ok); } +void* CurrentThreadId() { + std::uintptr_t id; + const auto self = scePthreadSelf(); + std::memcpy(&id, self, sizeof(id)); + return reinterpret_cast(id); +} + +template +void CheckUnsupportedInfo(TQuery query, void* object, std::uint32_t capacity = 4) { + TInfo info; + std::memset(&info, 0xEE, sizeof(info)); + UltWaitingThreadInfo waiters[4]; + std::memset(waiters, 0xAA, sizeof(waiters)); + for (auto& waiter : waiters) waiter.size = sizeof(UltWaitingThreadInfo); + std::uint8_t savedWaiters[sizeof(waiters)]; + std::memcpy(savedWaiters, waiters, sizeof(waiters)); + bool threw = false; + try { + query(object, &info, waiters, capacity); + } catch (const std::runtime_error&) { + threw = true; + } + CHECK(threw); + const auto* bytes = reinterpret_cast(&info); + for (std::size_t index = 0; index < sizeof(info); ++index) CHECK(bytes[index] == 0xEE); + CHECK(std::memcmp(waiters, savedWaiters, sizeof(waiters)) == 0); +} + +template +void CheckWaitingRecordSizes(TQuery query, void* object) { + TInfo info; + UltWaitingThreadInfo waiters[4]; + for (std::size_t invalid = 0; invalid < 4; ++invalid) { + for (const std::uint64_t size : {std::uint64_t{0}, std::uint64_t{63}, std::uint64_t{65}, UINT64_MAX}) { + std::memset(&info, 0xEE, sizeof(info)); + std::memset(waiters, 0xAA, sizeof(waiters)); + for (auto& waiter : waiters) waiter.size = sizeof(UltWaitingThreadInfo); + waiters[invalid].size = size; + std::uint8_t savedWaiters[sizeof(waiters)]; + std::memcpy(savedWaiters, waiters, sizeof(waiters)); + CHECK(query(object, &info, waiters, 4) == Invalid); + const auto* bytes = reinterpret_cast(&info); + for (std::size_t index = 0; index < sizeof(info); ++index) CHECK(bytes[index] == 0xEE); + CHECK(std::memcmp(waiters, savedWaiters, sizeof(waiters)) == 0); + } + } + CHECK(query(object, &info, waiters, 0) == Ok); + for (auto& waiter : waiters) waiter.size = sizeof(UltWaitingThreadInfo); + std::uint8_t savedWaiters[sizeof(waiters)]; + std::memcpy(savedWaiters, waiters, sizeof(waiters)); + CHECK(query(object, &info, waiters, 4) == Ok); + CHECK(std::memcmp(waiters, savedWaiters, sizeof(waiters)) == 0); +} + +void GetInfoTests() { + alignas(8) std::uint8_t dummy[512]{}; + alignas(8) std::uint8_t poolObj[256]{}; + alignas(8) std::uint8_t mutexObj[256]{}; + alignas(8) std::uint8_t condObj[256]{}; + alignas(8) std::uint8_t semObj[256]{}; + alignas(8) std::uint8_t rwObj[256]{}; + alignas(8) std::uint8_t dataPoolObj[512]{}; + alignas(8) std::uint8_t queueObj[512]{}; + alignas(8) std::uint8_t rtObj[4096]{}; + + UltMutexInfo mutexInfo{}; + UltConditionVariableInfo condInfo{}; + UltSemaphoreInfo semInfo{}; + UltReaderWriterLockInfo rwInfo{}; + UltWaitingQueueResourcePoolInfo poolInfo{}; + UltQueueDataResourcePoolInfo dataPoolInfo{}; + UltQueueInfo queueInfo{}; + UltUlthreadRuntimeInfo rtInfo{}; + UltUlthreadInfo threadInfo{}; + UltWaitingThreadInfo waitThreads[4]{}; + for (auto& waiter : waitThreads) waiter.size = sizeof(UltWaitingThreadInfo); + + CHECK(sceUltGetMutexInfo(nullptr, nullptr, nullptr, 0) == Null); + CHECK(sceUltGetMutexInfo(mutexObj, nullptr, nullptr, 0) == Null); + CHECK(sceUltGetMutexInfo(nullptr, &mutexInfo, nullptr, 0) == Null); + CHECK(sceUltGetMutexInfo(mutexObj, &mutexInfo, nullptr, 1) == Null); + CHECK(sceUltGetConditionVariableInfo(nullptr, nullptr, nullptr, 0) == Null); + CHECK(sceUltGetSemaphoreInfo(nullptr, nullptr, nullptr, 0) == Null); + CHECK(sceUltGetReaderWriterLockInfo(nullptr, nullptr, nullptr, 0) == Null); + CHECK(sceUltGetQueueInfo(nullptr, nullptr, nullptr, 0) == Null); + CHECK(sceUltGetWaitingQueueResourcePoolInfo(nullptr, nullptr) == Null); + CHECK(sceUltGetQueueDataResourcePoolInfo(nullptr, nullptr) == Null); + CHECK(sceUltGetUlthreadInfo(nullptr, nullptr) == Null); + CHECK(sceUltGetUlthreadRuntimeInfo(nullptr, nullptr, nullptr, 0, nullptr, 0) == Null); + CHECK(sceUltGetUlthreadRuntimeInfo(nullptr, &rtInfo, nullptr, 1, nullptr, 0) == Null); + CHECK(sceUltGetUlthreadRuntimeInfo(nullptr, &rtInfo, nullptr, 0, nullptr, 1) == Null); + + CHECK(sceUltGetMutexInfo(dummy, &mutexInfo, nullptr, 0) == State); + CHECK(sceUltGetConditionVariableInfo(dummy, &condInfo, nullptr, 0) == State); + CHECK(sceUltGetSemaphoreInfo(dummy, &semInfo, nullptr, 0) == State); + CHECK(sceUltGetReaderWriterLockInfo(dummy, &rwInfo, nullptr, 0) == State); + CHECK(sceUltGetWaitingQueueResourcePoolInfo(dummy, &poolInfo) == State); + CHECK(sceUltGetQueueDataResourcePoolInfo(dummy, &dataPoolInfo) == State); + CHECK(sceUltGetQueueInfo(dummy, &queueInfo, nullptr, 0) == State); + CHECK(sceUltGetUlthreadInfo(dummy, &threadInfo) == State); + CHECK(sceUltGetUlthreadRuntimeInfo(dummy, &rtInfo, nullptr, 0, nullptr, 0) == State); + + CHECK(sceUltWaitingQueueResourcePoolCreate(poolObj, "test-pool", 4, 4, nullptr, nullptr, 0) == Ok); + CHECK(sceUltGetWaitingQueueResourcePoolInfo(poolObj, &poolInfo) == Ok); + CHECK(poolInfo.size == sizeof(UltWaitingQueueResourcePoolInfo)); + CHECK(poolInfo.object == poolObj); + CHECK(std::strcmp(poolInfo.name, "test-pool") == 0); + CHECK(poolInfo.maxThreads == 4); + CHECK(poolInfo.maxSyncObjects == 4); + + CHECK(sceUltMutexCreate(mutexObj, "test-mutex", poolObj, nullptr, 0) == Ok); + CHECK(sceUltGetMutexInfo(mutexObj, &mutexInfo, waitThreads, 4) == Ok); + CHECK(mutexInfo.size == sizeof(UltMutexInfo)); + CHECK(mutexInfo.object == mutexObj); + CHECK(std::strcmp(mutexInfo.name, "test-mutex") == 0); + CHECK(mutexInfo.waitingQueuePool == poolObj); + CHECK(mutexInfo.lockCount == 0); + CHECK(mutexInfo.waitingThreads == 0); + CHECK(mutexInfo.owner == nullptr && mutexInfo.ownerType == 0); + CheckWaitingRecordSizes(sceUltGetMutexInfo, mutexObj); + + CHECK(sceUltMutexLock(mutexObj) == Ok); + CHECK(sceUltGetMutexInfo(mutexObj, &mutexInfo, nullptr, 0) == Ok); + CHECK(mutexInfo.lockCount == 0); + const auto owner = CurrentThreadId(); + CHECK(owner != nullptr && mutexInfo.owner == owner && mutexInfo.ownerType == 1); + std::thread([&] { + CHECK(CurrentThreadId() != owner); + UltMutexInfo held{}; + CHECK(sceUltGetMutexInfo(mutexObj, &held, nullptr, 0) == Ok); + CHECK(held.owner == owner && held.ownerType == 1 && held.lockCount == 0); + }).join(); + CHECK(sceUltMutexUnlock(mutexObj) == Ok); + CHECK(sceUltGetMutexInfo(mutexObj, &mutexInfo, nullptr, 0) == Ok); + CHECK(mutexInfo.owner == nullptr && mutexInfo.ownerType == 0 && mutexInfo.lockCount == 0); + + Object recursiveMutex{}; + UltMutexOptParam recursiveOptions{}; + recursiveOptions.attribute = 1; + CHECK(sceUltMutexCreate(&recursiveMutex, "recursive-info", poolObj, &recursiveOptions, 0) == Ok); + CHECK(sceUltMutexLock(&recursiveMutex) == Ok); + CheckUnsupportedInfo(sceUltGetMutexInfo, &recursiveMutex, 0); + CHECK(sceUltMutexUnlock(&recursiveMutex) == Ok); + CHECK(sceUltMutexDestroy(&recursiveMutex) == Ok); + + CHECK(sceUltConditionVariableCreate(condObj, "test-cnd", mutexObj, nullptr, 0) == Ok); + CHECK(sceUltGetConditionVariableInfo(condObj, &condInfo, waitThreads, 4) == Ok); + CHECK(condInfo.size == sizeof(UltConditionVariableInfo)); + CHECK(condInfo.object == condObj); + CHECK(condInfo.mutex == mutexObj); + CHECK(std::strcmp(condInfo.name, "test-cnd") == 0); + CHECK(condInfo.waitingThreads == 0); + + CHECK(sceUltSemaphoreCreate(semObj, "test-sem", 3, poolObj, nullptr, 0) == Ok); + CHECK(sceUltGetSemaphoreInfo(semObj, &semInfo, waitThreads, 4) == Ok); + CHECK(semInfo.size == sizeof(UltSemaphoreInfo)); + CHECK(semInfo.object == semObj); + CHECK(std::strcmp(semInfo.name, "test-sem") == 0); + CHECK(semInfo.waitingQueuePool == poolObj); + CHECK(semInfo.resources == 3); + + CHECK(sceUltReaderWriterLockCreate(rwObj, "test-rw", poolObj, nullptr, 0) == Ok); + CHECK(sceUltGetReaderWriterLockInfo(rwObj, &rwInfo, waitThreads, 4) == Ok); + CHECK(rwInfo.size == sizeof(UltReaderWriterLockInfo)); + CHECK(rwInfo.object == rwObj); + CHECK(std::strcmp(rwInfo.name, "test-rw") == 0); + CHECK(rwInfo.waitingQueuePool == poolObj); + CHECK(rwInfo.readers == 0); + CHECK(rwInfo.lockType == 0); + CHECK(rwInfo.owner == nullptr && rwInfo.ownerType == 0); + CheckWaitingRecordSizes(sceUltGetReaderWriterLockInfo, rwObj); + + CHECK(sceUltReaderWriterLockLockRead(rwObj) == Ok); + CHECK(sceUltGetReaderWriterLockInfo(rwObj, &rwInfo, nullptr, 0) == Ok); + CHECK(rwInfo.readers == 1); + CHECK(rwInfo.lockType == 1); + CHECK(rwInfo.owner == nullptr && rwInfo.ownerType == 0); + CHECK(sceUltReaderWriterLockLockRead(rwObj) == Ok); + CHECK(sceUltGetReaderWriterLockInfo(rwObj, &rwInfo, nullptr, 0) == Ok); + CHECK(rwInfo.lockType == 1 && rwInfo.readers == 2 && rwInfo.owner == nullptr && rwInfo.ownerType == 0); + CHECK(sceUltReaderWriterLockUnlockRead(rwObj) == Ok); + CHECK(sceUltReaderWriterLockUnlockRead(rwObj) == Ok); + CHECK(sceUltGetReaderWriterLockInfo(rwObj, &rwInfo, nullptr, 0) == Ok); + CHECK(rwInfo.lockType == 0 && rwInfo.readers == 0 && rwInfo.owner == nullptr && rwInfo.ownerType == 0); + for (const auto writeLock : {sceUltReaderWriterLockLockWrite, sceUltReaderWriterLockTryLockWrite}) { + CHECK(writeLock(rwObj) == Ok); + std::thread([&] { + UltReaderWriterLockInfo held{}; + CHECK(sceUltGetReaderWriterLockInfo(rwObj, &held, nullptr, 0) == Ok); + CHECK(held.lockType == 2 && held.readers == 0 && held.owner == owner && held.ownerType == 1); + }).join(); + CHECK(sceUltReaderWriterLockUnlockWrite(rwObj) == Ok); + CHECK(sceUltGetReaderWriterLockInfo(rwObj, &rwInfo, nullptr, 0) == Ok); + CHECK(rwInfo.lockType == 0 && rwInfo.owner == nullptr && rwInfo.ownerType == 0); + } + + CHECK(sceUltQueueDataResourcePoolCreate(dataPoolObj, "test-datapool", 8, 64, 2, poolObj, nullptr, nullptr, 0) == Ok); + CHECK(sceUltGetQueueDataResourcePoolInfo(dataPoolObj, &dataPoolInfo) == Ok); + CHECK(dataPoolInfo.size == sizeof(UltQueueDataResourcePoolInfo)); + CHECK(dataPoolInfo.object == dataPoolObj); + CHECK(std::strcmp(dataPoolInfo.name, "test-datapool") == 0); + CHECK(dataPoolInfo.waitingQueuePool == poolObj); + CHECK(dataPoolInfo.dataSize == 64); + CHECK(dataPoolInfo.maxItems == 8); + CHECK(dataPoolInfo.availableItems == 8); + CHECK(dataPoolInfo.maxQueues == 2); + + CHECK(sceUltQueueCreate(queueObj, "test-queue", 64, poolObj, dataPoolObj, nullptr, 0) == Ok); + CHECK(sceUltGetQueueInfo(queueObj, &queueInfo, waitThreads, 4) == Ok); + CHECK(queueInfo.size == sizeof(UltQueueInfo)); + CHECK(queueInfo.object == queueObj); + CHECK(std::strcmp(queueInfo.name, "test-queue") == 0); + CHECK(queueInfo.waitingQueuePool == poolObj); + CHECK(queueInfo.dataPool == dataPoolObj); + CHECK(queueInfo.dataSize == 64); + CHECK(queueInfo.queuedItems == 0); + + std::uint8_t itemData[64]{0x42}; + CHECK(sceUltQueuePush(queueObj, itemData) == Ok); + CHECK(sceUltGetQueueInfo(queueObj, &queueInfo, nullptr, 0) == Ok); + CHECK(queueInfo.queuedItems == 1); + CHECK(sceUltQueuePop(queueObj, itemData) == Ok); + CHECK(sceUltGetQueueInfo(queueObj, &queueInfo, nullptr, 0) == Ok); + CHECK(queueInfo.queuedItems == 0); + + CHECK(sceUltUlthreadRuntimeCreate(rtObj, "test-rt", 2, 1, nullptr, nullptr, 0) == Ok); + void* activeThreads[4]{}; + CHECK(sceUltGetUlthreadRuntimeInfo(rtObj, &rtInfo, activeThreads, 4, nullptr, 0) == Ok); + CHECK(rtInfo.size == sizeof(UltUlthreadRuntimeInfo)); + CHECK(rtInfo.object == rtObj); + CHECK(std::strcmp(rtInfo.name, "test-rt") == 0); + CHECK(rtInfo.maxUlthreads == 2); + CHECK(rtInfo.workerThreads == 1); + CHECK(rtInfo.activeUlthreads == 0); + + Pthread workerHandles[2]{}; + bool workerQueryThrew = false; + try { + sceUltGetUlthreadRuntimeInfo(rtObj, &rtInfo, nullptr, 0, workerHandles, 2); + } catch (const std::runtime_error&) { + workerQueryThrew = true; + } + CHECK(workerQueryThrew); + + UltUlthreadRuntimeInfo untouchedInfo{}; + std::memset(&untouchedInfo, 0xEE, sizeof(untouchedInfo)); + CHECK(sceUltGetUlthreadRuntimeInfo(rtObj, &untouchedInfo, nullptr, 0, nullptr, 1) == Null); + const auto* infoBytes = reinterpret_cast(&untouchedInfo); + for (std::size_t i = 0; i < sizeof(untouchedInfo); ++i) { + CHECK(infoBytes[i] == 0xEE); + } + + CHECK(sceUltConditionVariableDestroy(condObj) == Ok); + CHECK(sceUltMutexDestroy(mutexObj) == Ok); + CHECK(sceUltSemaphoreDestroy(semObj) == Ok); + CHECK(sceUltReaderWriterLockDestroy(rwObj) == Ok); + CHECK(sceUltQueueDestroy(queueObj) == Ok); + CHECK(sceUltQueueDataResourcePoolDestroy(dataPoolObj) == Ok); + CHECK(sceUltWaitingQueueResourcePoolDestroy(poolObj) == Ok); + CHECK(sceUltUlthreadRuntimeDestroy(rtObj) == Ok); +} + +void WaitForQueueWaiters(std::uint8_t* queue, UltQueueInfo* info, std::uint32_t pushers, std::uint32_t poppers) { + for (int attempt = 0; attempt < 4000; ++attempt) { + CHECK(sceUltGetQueueInfo(queue, info, nullptr, 0) == Ok); + if (info->waitingPushers == pushers && info->waitingPoppers == poppers) return; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + CHECK(false); +} + +void QueueWaiters() { + alignas(8) std::uint8_t poolObj[256]{}; + alignas(8) std::uint8_t dataPoolObj[512]{}; + alignas(8) std::uint8_t queueObj[512]{}; + UltQueueInfo info{}; + UltWaitingThreadInfo waiters[4]{}; + + CHECK(sceUltWaitingQueueResourcePoolCreate(poolObj, "waiter-pool", 4, 4, nullptr, nullptr, 0) == Ok); + CHECK(sceUltQueueDataResourcePoolCreate(dataPoolObj, "waiter-data", 2, 64, 1, poolObj, nullptr, nullptr, 0) == Ok); + CHECK(sceUltQueueCreate(queueObj, "waiter-queue", 64, poolObj, dataPoolObj, nullptr, 0) == Ok); + + std::uint8_t item[64]{0x11}; + std::uint8_t popped[64]{}; + + std::atomic popResult{1}; + std::thread popper([&] { popResult.store(sceUltQueuePop(queueObj, popped)); }); + WaitForQueueWaiters(queueObj, &info, 0, 1); + CHECK(info.queuedItems == 0); + std::memset(waiters, 0xAA, sizeof(waiters)); + CheckUnsupportedInfo(sceUltGetQueueInfo, queueObj, 1); + CheckUnsupportedInfo(sceUltGetQueueInfo, queueObj); + CHECK(sceUltGetQueueInfo(queueObj, &info, waiters, 0) == Ok); + const auto* waiterBytes = reinterpret_cast(waiters); + for (std::size_t index = 0; index < sizeof(waiters); ++index) CHECK(waiterBytes[index] == 0xAA); + CHECK(sceUltQueueDestroy(queueObj) == State); + CHECK(sceUltQueuePush(queueObj, item) == Ok); + popper.join(); + CHECK(popResult.load() == Ok); + CHECK(popped[0] == 0x11); + WaitForQueueWaiters(queueObj, &info, 0, 0); + + CHECK(sceUltQueuePush(queueObj, item) == Ok); + CHECK(sceUltQueuePush(queueObj, item) == Ok); + + std::atomic pushResult{1}; + std::thread pusher([&] { pushResult.store(sceUltQueuePush(queueObj, item)); }); + WaitForQueueWaiters(queueObj, &info, 1, 0); + CHECK(info.queuedItems == 2); + CheckUnsupportedInfo(sceUltGetQueueInfo, queueObj, 1); + CheckUnsupportedInfo(sceUltGetQueueInfo, queueObj); + CHECK(sceUltGetQueueInfo(queueObj, &info, waiters, 0) == Ok); + CHECK(info.waitingPushers == 1); + CHECK(info.waitingPoppers == 0); + CHECK(sceUltQueuePop(queueObj, popped) == Ok); + pusher.join(); + CHECK(pushResult.load() == Ok); + WaitForQueueWaiters(queueObj, &info, 0, 0); + CHECK(info.queuedItems == 2); + + CHECK(sceUltQueuePop(queueObj, popped) == Ok); + CHECK(sceUltQueuePop(queueObj, popped) == Ok); + CHECK(sceUltQueueDestroy(queueObj) == Ok); + CHECK(sceUltQueueDataResourcePoolDestroy(dataPoolObj) == Ok); + CHECK(sceUltWaitingQueueResourcePoolDestroy(poolObj) == Ok); +} + +template +void WaitForInfoWaiters(TQuery query, void* object) { + for (int attempt = 0; attempt < 4000; ++attempt) { + TInfo info{}; + CHECK(query(object, &info, nullptr, 0) == Ok); + if (info.waitingThreads == 1) return; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + CHECK(false); +} + +void InfoWaiters() { + Object pool{}; + Object mutex{}; + Object condition{}; + Object semaphore{}; + Object rwlock{}; + CHECK(sceUltWaitingQueueResourcePoolCreate(&pool, "info-pool", 4, 4, nullptr, nullptr, 0) == Ok); + CHECK(sceUltMutexCreate(&mutex, "info-mutex", &pool, nullptr, 0) == Ok); + CHECK(sceUltConditionVariableCreate(&condition, "info-condition", &mutex, nullptr, 0) == Ok); + CHECK(sceUltSemaphoreCreate(&semaphore, "info-semaphore", 0, &pool, nullptr, 0) == Ok); + CHECK(sceUltReaderWriterLockCreate(&rwlock, "info-rwlock", &pool, nullptr, 0) == Ok); + + CHECK(sceUltMutexLock(&mutex) == Ok); + std::thread mutexWaiter([&] { + CHECK(sceUltMutexLock(&mutex) == Ok); + CHECK(sceUltMutexUnlock(&mutex) == Ok); + }); + WaitForInfoWaiters(sceUltGetMutexInfo, &mutex); + CheckUnsupportedInfo(sceUltGetMutexInfo, &mutex, 1); + CheckUnsupportedInfo(sceUltGetMutexInfo, &mutex); + CHECK(sceUltMutexUnlock(&mutex) == Ok); + mutexWaiter.join(); + + std::thread conditionWaiter([&] { + CHECK(sceUltMutexLock(&mutex) == Ok); + CHECK(sceUltConditionVariableWait(&condition) == Ok); + UltMutexInfo info{}; + CHECK(sceUltGetMutexInfo(&mutex, &info, nullptr, 0) == Ok); + CHECK(info.owner == CurrentThreadId() && info.ownerType == 1); + CHECK(sceUltMutexUnlock(&mutex) == Ok); + }); + WaitForInfoWaiters(sceUltGetConditionVariableInfo, &condition); + CheckUnsupportedInfo(sceUltGetConditionVariableInfo, &condition, 1); + CheckUnsupportedInfo(sceUltGetConditionVariableInfo, &condition); + CHECK(sceUltConditionVariableSignal(&condition) == Ok); + conditionWaiter.join(); + + std::thread semaphoreWaiter([&] { CHECK(sceUltSemaphoreAcquire(&semaphore, 1) == Ok); }); + WaitForInfoWaiters(sceUltGetSemaphoreInfo, &semaphore); + CheckUnsupportedInfo(sceUltGetSemaphoreInfo, &semaphore, 1); + CheckUnsupportedInfo(sceUltGetSemaphoreInfo, &semaphore); + CHECK(sceUltSemaphoreRelease(&semaphore, 1) == Ok); + semaphoreWaiter.join(); + + CHECK(sceUltReaderWriterLockLockWrite(&rwlock) == Ok); + std::thread reader([&] { + CHECK(sceUltReaderWriterLockLockRead(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockUnlockRead(&rwlock) == Ok); + }); + WaitForInfoWaiters(sceUltGetReaderWriterLockInfo, &rwlock); + CheckUnsupportedInfo(sceUltGetReaderWriterLockInfo, &rwlock, 1); + CheckUnsupportedInfo(sceUltGetReaderWriterLockInfo, &rwlock); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == Ok); + reader.join(); + + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Ok); + CHECK(sceUltSemaphoreDestroy(&semaphore) == Ok); + CHECK(sceUltConditionVariableDestroy(&condition) == Ok); + CHECK(sceUltMutexDestroy(&mutex) == Ok); + CHECK(sceUltWaitingQueueResourcePoolDestroy(&pool) == Ok); +} + } int main(int argc, char** argv) { @@ -282,6 +684,9 @@ int main(int argc, char** argv) { else if (std::strcmp(argv[1], "rwlock-pool") == 0) ReaderWriterPool(); else if (std::strcmp(argv[1], "rwlock-finalize") == 0) ReaderWriterFinalize(); else if (std::strcmp(argv[1], "ulthreads") == 0) Ulthreads(); + else if (std::strcmp(argv[1], "get-info") == 0) GetInfoTests(); + else if (std::strcmp(argv[1], "queue-waiters") == 0) QueueWaiters(); + else if (std::strcmp(argv[1], "info-waiters") == 0) InfoWaiters(); else return 2; CHECK(sceUltFinalize() == Ok); std::puts("ULT sync tests passed"); diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index 50fef67d4c..23b0e358fb 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -86,14 +86,7 @@ static void Validation() { CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(1, 1) == 1184); CHECK(sceUltConditionVariableDestroy(nullptr) == -2139029503); CHECK(sceUltReaderWriterLockDestroy(nullptr) == Null); - bool threwInfo = false; - try { - sceUltGetConditionVariableInfo(nullptr, nullptr, nullptr, 0); - - } catch (const std::runtime_error&) { - threwInfo = true; - } - CHECK(threwInfo); + CHECK(sceUltGetConditionVariableInfo(nullptr, nullptr, nullptr, 0) == Null); } static void TryJoin() { diff --git a/core/libs/tests/GuestUnderflowError.cpp b/core/libs/tests/GuestUnderflowError.cpp new file mode 100644 index 0000000000..b1bfda125c --- /dev/null +++ b/core/libs/tests/GuestUnderflowError.cpp @@ -0,0 +1,36 @@ +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +struct TypeRecord { const void* const* vtable; const char* name; const TypeRecord* base; }; +struct Vtable { std::ptrdiff_t offset; const TypeRecord* type; const void* destroy; const void* deleteObject; const void* what; }; + +extern "C" { +extern const TypeRecord _ZTISt13runtime_error_nid_postfix; +extern const TypeRecord _ZTISt15underflow_error_nid_postfix; +extern const Vtable _ZTVSt13runtime_error_nid_postfix; +extern const Vtable _ZTVSt15underflow_error_nid_postfix; +} + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + const auto& type = _ZTISt15underflow_error_nid_postfix; + const auto& runtimeType = _ZTISt13runtime_error_nid_postfix; + Require(std::strcmp(type.name, "St15underflow_error") == 0); + Require(type.base == &runtimeType && type.vtable == runtimeType.vtable); + const auto& table = _ZTVSt15underflow_error_nid_postfix; + const auto& runtimeTable = _ZTVSt13runtime_error_nid_postfix; + Require(table.offset == 0 && table.type == &type); + Require(table.destroy == runtimeTable.destroy && table.deleteObject == runtimeTable.deleteObject && table.what == runtimeTable.what); + LibcException::ExceptionObject object{}; + _ZNSt13runtime_errorC1EPKc_nid_postfix(&object, "value too small"); + Require(object.message != nullptr && std::strcmp(object.message, "value too small") == 0); + _ZNSt15underflow_errorD1Ev_nid_postfix(&object); + Require(object.message == nullptr); + _ZNSt15underflow_errorD1Ev_nid_postfix(&object); + Require(object.message == nullptr); + return 0; +} diff --git a/core/libs/tests/GuestUsbd.cpp b/core/libs/tests/GuestUsbd.cpp index 512bee8071..eb6cc9af44 100644 --- a/core/libs/tests/GuestUsbd.cpp +++ b/core/libs/tests/GuestUsbd.cpp @@ -1,6 +1,7 @@ #include "prx/libSceUsbd/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -18,6 +19,10 @@ namespace { namespace { constexpr std::int32_t invalidArgument = static_cast(0x80240002); +constexpr std::int32_t noDevice = static_cast(0x80240004); +constexpr std::int32_t notFound = static_cast(0x80240005); +constexpr std::int32_t otherError = static_cast(0x802400FF); +constexpr std::uint8_t absentEndpoint = 0x70; void Require(bool condition, const char* message) { if (!condition) { @@ -87,6 +92,10 @@ void Device() { void* handle = nullptr; Require(sceUsbdOpen(device, &handle) == 0 && handle != nullptr, "opening the device failed"); Require(sceUsbdCheckConnected(handle) == 0, "an open device was reported disconnected"); + Require(sceUsbdGetDevice(handle) == device, "the device of an open handle is not the device it was opened from"); + const int speed = sceUsbdGetDeviceSpeed(device); + Require(speed >= 1 && speed <= 4, "the device speed is not low, full, high or super speed"); + std::printf("device speed %d\n", speed); std::int32_t configuration = -1; Require(sceUsbdGetConfiguration(handle, &configuration) == 0 && configuration == activeValue, "configuration differs from the active config descriptor"); std::uint8_t text[256] = {}; @@ -131,6 +140,7 @@ void Device() { const std::uint8_t* interfaceArray = *reinterpret_cast(config0 + 16); std::uint8_t interruptAddress = 0; std::uint16_t interruptMax = 0; + std::uint8_t interruptInterface = 0; if (interfaceArray == nullptr) { Require(false, "the active config descriptor has no interface array"); } @@ -144,11 +154,29 @@ void Device() { if ((item[2] & 0x80) != 0 && (item[3] & 3) == 3) { interruptAddress = item[2]; interruptMax = static_cast(item[4] | item[5] << 8); + interruptInterface = alternate[2]; } } } Require(interruptAddress != 0 && interruptMax != 0, "the device has no interrupt IN endpoint"); sceUsbdFreeConfigDescriptor(config); + Require(sceUsbdGetMaxPacketSize(device, interruptAddress) == interruptMax, "the max packet size differs from the endpoint descriptor"); + const int expectedIso = speed <= 2 ? interruptMax : (interruptMax & 0x7FF) * std::min(1 + ((interruptMax >> 11) & 3), 3); + Require(sceUsbdGetMaxIsoPacketSize(device, interruptAddress) == expectedIso, "the max iso packet size differs from the endpoint descriptor"); + Require(sceUsbdGetMaxPacketSize(device, absentEndpoint) == notFound && sceUsbdGetMaxIsoPacketSize(device, absentEndpoint) == notFound, "an absent endpoint has a max packet size"); + Require(sceUsbdSetInterfaceAltSetting(handle, interruptInterface, 0) == notFound, "an unclaimed interface took an alternate setting"); + Require(sceUsbdSetInterfaceAltSetting(handle, 32, 0) == invalidArgument && sceUsbdSetInterfaceAltSetting(handle, -1, 0) == invalidArgument, "an interface number outside 0 to 31 was accepted"); + Require(sceUsbdClaimInterface(handle, interruptInterface) == 0, "claiming the interrupt endpoint's interface failed"); + const std::int32_t alternate = sceUsbdSetInterfaceAltSetting(handle, interruptInterface, 0); + Require(alternate == 0 || alternate == otherError, "alternate setting 0 returned neither success nor the other error"); + std::printf("alternate setting 0 of interface %u: %d\n", interruptInterface, alternate); + Require(sceUsbdClearHalt(handle, interruptAddress) == 0, "clearing the interrupt endpoint's halt failed"); + Require(sceUsbdClearHalt(handle, absentEndpoint) == otherError, "clearing the halt of an absent endpoint did not return the other error"); + const std::int32_t detached = sceUsbdDetachKernelDriver(handle, interruptInterface); + Require(detached == 0 || detached == otherError, "detaching the kernel driver returned neither success nor the other error"); + std::printf("kernel driver detach: %d\n", detached); + Require(sceUsbdReleaseInterface(handle, interruptInterface) == 0, "releasing the interrupt endpoint's interface failed"); + Require(sceUsbdSetInterfaceAltSetting(handle, interruptInterface, 0) == notFound, "a released interface took an alternate setting"); auto* transfer = reinterpret_cast(sceUsbdAllocTransfer(0)); std::uint8_t setup[8 + 18] = {0x80, 6, 0x00, 0x01, 0x00, 0x00, 18, 0}; @@ -196,7 +224,17 @@ void Device() { sceUsbdClose(handle); Require(Throws([&] { sceUsbdClose(handle); }), "a handle was closed twice"); + Require(Throws([&] { sceUsbdGetDevice(handle); }), "the device of a closed handle was returned"); sceUsbdUnrefDevice(device); + void* byId = sceUsbdOpenDeviceWithVidPid(static_cast(vendor), static_cast(product)); + Require(byId != nullptr, "opening the device by vendor and product failed"); + void* byIdDevice = sceUsbdGetDevice(byId); + std::uint8_t byIdDescriptor[18] = {}; + Require(sceUsbdGetDeviceDescriptor(byIdDevice, byIdDescriptor) == 0, "the device of a handle opened by vendor and product has no descriptor"); + Require((byIdDescriptor[8] | byIdDescriptor[9] << 8) == vendor && (byIdDescriptor[10] | byIdDescriptor[11] << 8) == product, "the device opened by vendor and product has another vendor or product"); + Require(sceUsbdGetMaxPacketSize(byIdDevice, interruptAddress) == interruptMax, "the max packet size through a handle's device differs"); + sceUsbdClose(byId); + Require(Throws([&] { sceUsbdGetDeviceDescriptor(byIdDevice, byIdDescriptor); }), "the device of a closed handle stayed known"); std::puts("USBD device tests passed"); } @@ -291,6 +329,21 @@ int main(int argc, char** argv) { Require(Throws([&] { sceUsbdSubmitTransfer(reinterpret_cast(transfer)); }), "submitting on a handle that was never opened did not throw"); sceUsbdFreeTransfer(reinterpret_cast(transfer)); + Require(Throws([&] { sceUsbdClearHalt(nullptr, 0x81); }), "clearing a halt on a null handle did not throw"); + Require(Throws([&] { sceUsbdClearHalt(&foreign, 0x81); }), "clearing a halt on a handle that was never opened did not throw"); + Require(sceUsbdDetachKernelDriver(nullptr, 0) == invalidArgument, "null handle kernel driver detach accepted"); + Require(Throws([&] { sceUsbdDetachKernelDriver(&foreign, 0); }), "kernel driver detach on a handle that was never opened did not throw"); + Require(sceUsbdGetDevice(nullptr) == nullptr, "a null handle has a device"); + Require(Throws([&] { sceUsbdGetDevice(&foreign); }), "the device of a handle that was never opened did not throw"); + Require(sceUsbdGetDeviceSpeed(nullptr) == 0, "a null device has a speed"); + Require(Throws([&] { sceUsbdGetDeviceSpeed(&foreign); }), "the speed of a device that was never listed did not throw"); + Require(sceUsbdGetMaxPacketSize(nullptr, 0x81) == noDevice, "a null device has a max packet size"); + Require(Throws([&] { sceUsbdGetMaxPacketSize(&foreign, 0x81); }), "the max packet size of a device that was never listed did not throw"); + Require(Throws([&] { sceUsbdGetMaxIsoPacketSize(nullptr, 0x81); }), "the max iso packet size of a null device did not throw"); + Require(Throws([&] { sceUsbdGetMaxIsoPacketSize(&foreign, 0x81); }), "the max iso packet size of a device that was never listed did not throw"); + Require(sceUsbdSetInterfaceAltSetting(nullptr, 0, 0) == invalidArgument, "null handle alternate setting accepted"); + Require(Throws([&] { sceUsbdSetInterfaceAltSetting(&foreign, 0, 0); }), "an alternate setting on a handle that was never opened did not throw"); + Require(sceUsbdOpenDeviceWithVidPid(0, 0) == nullptr, "a device with vendor and product 0 was opened"); sceUsbdFreeTransfer(nullptr); Require(sceUsbdEventHandlingOk() == 1, "event handling not allowed"); @@ -306,6 +359,7 @@ int main(int argc, char** argv) { Require(setupBuffer[6] == 18 && setupBuffer[7] == 0, "length not filled little-endian"); if (argc == 2 && std::strcmp(argv[1], "device") == 0) Device(); sceUsbdExit(); + Require(sceUsbdOpenDeviceWithVidPid(0, 0) == nullptr, "a device was opened after the library exited"); std::puts("USBD tests passed"); return 0; diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index d42de627a9..9b46dffc13 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -179,6 +179,15 @@ int main() { Require(sceUserServiceGetUserColor(USER_SERVICE_INITIAL_USER_ID + 1, &color) == USER_SERVICE_OK); Require(color == 1); + Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID + 1, &age_level) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID + 1, nullptr) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + 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); + Require(value == 0x5a5a5a5a); + Require(setting.get(USER_SERVICE_INITIAL_USER_ID + 1, nullptr) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + } + UserServiceSetUserConnected_nid_postfix(1, true); Require(sceUserServiceGetRegisteredUserIdList(®istered) == USER_SERVICE_OK); Require(registered.user_id[0] == USER_SERVICE_INITIAL_USER_ID); @@ -186,10 +195,26 @@ int main() { for (int i = 2; i < 16; ++i) { Require(registered.user_id[i] == USER_SERVICE_USER_ID_INVALID); } + Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID + 1, &age_level) == USER_SERVICE_OK); + Require(age_level == 0xffffffffu); + for (const Setting& setting : settings) { + std::int32_t value = 0x5a5a5a5a; + Require(setting.get(USER_SERVICE_INITIAL_USER_ID + 1, &value) == USER_SERVICE_OK); + Require(value == setting.value); + } + UserServiceSetUserConnected_nid_postfix(1, false); Require(sceUserServiceGetRegisteredUserIdList(®istered) == USER_SERVICE_OK); Require(registered.user_id[0] == USER_SERVICE_INITIAL_USER_ID); for (int i = 1; i < 16; ++i) { Require(registered.user_id[i] == USER_SERVICE_USER_ID_INVALID); } + Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID + 1, &age_level) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID + 1, nullptr) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + 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); + Require(value == 0x5a5a5a5a); + Require(setting.get(USER_SERVICE_INITIAL_USER_ID + 1, nullptr) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + } } diff --git a/core/libs/tests/GuestVdecsw.cpp b/core/libs/tests/GuestVdecsw.cpp index d30d2421aa..0990141df2 100644 --- a/core/libs/tests/GuestVdecsw.cpp +++ b/core/libs/tests/GuestVdecsw.cpp @@ -1,9 +1,11 @@ -#include "prx/libSceVdecsw/include/Api.hpp" +#include "prx/libSceVdecsw/libSceVdecsw.hpp" +#include #include #include #include #include #include +#include #include #include #include @@ -60,6 +62,102 @@ struct Session { } }; +template +bool throws(TAction action) { + try { + action(); + } catch (const std::exception&) { + return true; + } + return false; +} + +template +void checkGenericPictureInfo(const OutputInfo& output, const TInfo& typed) { + const std::array sizes{8, 9, 0x28, sizeof(TInfo) - 1, sizeof(TInfo), sizeof(TInfo) + 16, std::numeric_limits::max()}; + for (const auto size : sizes) { + std::array storage; + storage.fill(0xcc); + std::memcpy(storage.data() + 1, &size, sizeof(size)); + auto expected = storage; + const auto copied = std::min(size, sizeof(TInfo)) - sizeof(size); + std::memcpy(expected.data() + 1 + sizeof(size), reinterpret_cast(&typed) + sizeof(size), copied); + auto second = storage; + const auto secondBefore = second; + check(sceVdecswGetPictureInfo(&output, storage.data() + 1, second.data()) == 0, "generic picture info query failed"); + check(storage == expected && second == secondBefore, "generic picture info bytes, size or guards differ"); + } + for (std::uint64_t size = 0; size < sizeof(size); ++size) { + auto tooSmall = typed; + tooSmall.thisSize = size; + const auto before = tooSmall; + check(throws([&] { sceVdecswGetPictureInfo(&output, &tooSmall, nullptr); }), "generic picture info accepted an undersized size field"); + check(std::memcmp(&tooSmall, &before, sizeof(tooSmall)) == 0, "an invalid generic query changed its output"); + } + auto second = typed; + const auto secondBefore = second; + check(sceVdecswGetPictureInfo(&output, nullptr, &second) == 0, "generic picture info rejected a null first output"); + check(std::memcmp(&second, &secondBefore, sizeof(second)) == 0, "generic picture info wrote a nonexistent second picture"); + auto shorter = output; + shorter.thisSize = sizeof(OutputInfo) - 8; + auto actual = typed; + check(sceVdecswGetPictureInfo(&shorter, &actual, nullptr) == 0 && std::memcmp(&actual, &typed, sizeof(actual)) == 0, "generic picture info rejected the shorter output descriptor"); +} + +void testGenericPictureInfoValidation() { + OutputInfo output{sizeof(OutputInfo)}; + std::array first; + first.fill(0xccccccccccccccccull); + const auto before = first; + auto second = first; + check(throws([&] { sceVdecswGetPictureInfo(nullptr, first.data(), second.data()); }), "generic picture info accepted a null output descriptor"); + check(first == before && second == before, "a null descriptor changed picture info"); + check(sceVdecswGetPictureInfo(&output, first.data(), second.data()) == 0 && first == before && second == before, "an empty output changed picture info"); + check(sceVdecswGetPictureInfo(&output, nullptr, nullptr) == 0, "an empty output required picture-info pointers"); + for (const auto size : {0ull, 0x28ull, 0x31ull, 0x40ull, 0x100000038ull}) { + output.thisSize = size; + check(throws([&] { sceVdecswGetPictureInfo(&output, first.data(), second.data()); }) && first == before && second == before, "generic picture info accepted an invalid descriptor size"); + } + output.thisSize = sizeof(OutputInfo); + first[0] = sizeof(first); + const auto firstBefore = first; + output.pictureCount = 1; + output.frameBuffer = first.data(); + check(throws([&] { sceVdecswGetPictureInfo(&output, first.data(), nullptr); }) && first == firstBefore, "generic picture info accepted a foreign frame buffer"); + output.frameBuffer = nullptr; + check(throws([&] { sceVdecswGetPictureInfo(&output, first.data(), nullptr); }) && first == firstBefore, "generic picture info accepted a null frame buffer"); +} + +void testGenericPictureInfoLifetime() { + OutputInfo output{sizeof(OutputInfo)}; + std::vector buffer; + { + Session session; + buffer.resize(session.memory.maxFrameBufferSize); + const auto units = accessUnits(false); + for (std::uint64_t iteration = 0; iteration < 2; ++iteration) { + const SceVideodec2::InputData input{sizeof(SceVideodec2::InputData), units[0].data(), units[0].size(), 1000 + iteration, iteration, 0xa0 + iteration}; + check(sceVdecswSetDecodeInput(session.decoder, &input) == 0, "generic lifetime input failed"); + InputResult consumed{sizeof(InputResult)}; + check(sceVdecswSyncDecodeInput(session.decoder, &consumed) == 0, "generic lifetime input was not consumed"); + const FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size()}; + check(sceVdecswSetDecodeOutput(session.decoder, &frame) == 0, "generic lifetime buffer failed"); + check(sceVdecswFinalizeDecodeSequence(session.decoder) == 0, "generic lifetime finalize failed"); + check(sceVdecswSyncDecodeOutput(session.decoder, &output) == 0 && output.isValid, "generic lifetime picture missing"); + AvcPictureInfo info{sizeof(AvcPictureInfo)}; + check(sceVdecswGetPictureInfo(&output, &info, nullptr) == 0 && info.ptsData == input.ptsData && info.dtsData == iteration && info.attachedData == input.attachedData, "generic picture info retained stale timestamps"); + if (iteration == 0) { + check(sceVdecswResetDecoder(session.decoder) == 0, "generic lifetime reset failed"); + const auto before = info; + check(throws([&] { sceVdecswGetPictureInfo(&output, &info, nullptr); }) && std::memcmp(&info, &before, sizeof(info)) == 0, "generic AVC picture info survived a decoder reset"); + } + } + } + AvcPictureInfo info{sizeof(AvcPictureInfo)}; + const auto before = info; + check(throws([&] { sceVdecswGetPictureInfo(&output, &info, nullptr); }) && std::memcmp(&info, &before, sizeof(info)) == 0, "generic picture info survived decoder destruction"); +} + void testDecode() { Session session; const auto units = accessUnits(false); @@ -92,6 +190,7 @@ void testDecode() { check(!output.isErrorFrame && !output.isDiscardedFrame && output.pictureCount == 1 && output.frameWidth == Width && output.frameHeight == CodedHeight && output.frameBuffer == buffers[pictures % 2].data(), "unexpected picture geometry"); AvcPictureInfo info{sizeof(AvcPictureInfo)}; check(sceVdecswGetAvcPictureInfo(&output, &info, nullptr) == 0 && info.isValid, "picture info missing"); + checkGenericPictureInfo(output, info); const auto unit = DisplayOrderUnits[pictures]; check(info.ptsData == 1000 + unit && info.dtsData == unit && info.attachedData == 0xa0 + unit && info.idrPictureFlag == (pictures == 0 ? 1 : 0) && info.profileIdc == 100, "pictures out of display order"); check(info.picWidthInMbsMinus1 == Width / 16 - 1 && info.picHeightInMapUnitsMinus1 == CodedHeight / 16 - 1 && info.frameMbsOnlyFlag == 1 && info.frameCroppingFlag == 1 && info.frameCropLeftOffset == 0 && info.frameCropRightOffset == 0 && info.frameCropTopOffset == 0 && info.frameCropBottomOffset == (CodedHeight - Height) / 2, "picture geometry in the picture info is wrong"); @@ -138,6 +237,7 @@ void testHevc() { check(pictures < HevcHashes.size() && output.codecType == CodecHevc && output.pictureCount == 1 && output.frameWidth == Width && output.frameHeight == Height, "unexpected HEVC picture geometry"); HevcPictureInfo info{sizeof(HevcPictureInfo)}; check(sceVdecswGetHevcPictureInfo(&output, &info) == 0 && info.isValid, "HEVC picture info missing"); + checkGenericPictureInfo(output, info); const auto unit = HevcDisplayOrderUnits[pictures]; check(info.ptsData == 1000 + unit && info.dtsData == unit && info.attachedData == 0xa0 + unit, "HEVC pictures out of display order"); check(info.idrPictureFlag == (pictures == 0 ? 1 : 0) && info.irapPictureFlag == (pictures == 0 ? 1 : 0), "HEVC random access flags differ"); @@ -182,6 +282,7 @@ void testHevcCrop() { check(sceVdecswSyncDecodeOutput(session.decoder, &output) == 0 && output.isValid && output.frameWidth == 64 && output.frameHeight == 40, "unexpected cropped HEVC picture geometry"); HevcPictureInfo info{sizeof(HevcPictureInfo)}; check(sceVdecswGetHevcPictureInfo(&output, &info) == 0 && info.isValid, "cropped HEVC picture info missing"); + checkGenericPictureInfo(output, info); check(info.picWidthInLumaSamples == 64 && info.picHeightInLumaSamples == 40, "cropped HEVC coded size differs"); check(info.conformanceWindowFlag == 1 && info.confWinLeftOffset == 0 && info.confWinRightOffset == 0 && info.confWinTopOffset == 0 && info.confWinBottomOffset == 2, "HEVC conformance window differs"); check(info.frameCroppingFlag == 1 && info.frameCropLeftOffset == 0 && info.frameCropRightOffset == 0 && info.frameCropTopOffset == 0 && info.frameCropBottomOffset == 2, "HEVC frame cropping differs"); @@ -253,16 +354,6 @@ struct Vp9Session { } }; -template -bool throws(TAction action) { - try { - action(); - } catch (const std::exception&) { - return true; - } - return false; -} - SceVideodec2::Vp9PictureInfo sentinelVp9Info(std::uint64_t size = sizeof(SceVideodec2::Vp9PictureInfo)) { SceVideodec2::Vp9PictureInfo info; std::memset(&info, 0xcc, sizeof(info)); @@ -314,6 +405,7 @@ void testVp9PictureInfo() { check(output.codecType == CodecVp9 && output.pictureCount == 1 && output.frameWidth == Width && output.frameHeight == Height && output.frameBuffer == target.data(), "unexpected VP9 picture geometry"); auto info = sentinelVp9Info(); check(sceVdecswGetVp9PictureInfo(&output, &info) == 0, "VP9 picture info query failed"); + checkGenericPictureInfo(output, info); checkVp9Info(info, pictures); check(hashNv12(target.data(), output.framePitch, output.frameHeight) == Vp9Hashes[pictures], "VP9 picture " + std::to_string(pictures) + " differs from the reference"); outputs[pictures] = output; @@ -335,6 +427,7 @@ void testVp9PictureInfo() { auto info = sentinelVp9Info(); check(sceVdecswGetVp9PictureInfo(&outputs[picture], &info) == 0, "VP9 picture info of an earlier picture was lost"); checkVp9Info(info, picture); + checkGenericPictureInfo(outputs[picture], info); } const auto unchanged = sentinelVp9Info(); @@ -380,6 +473,7 @@ void testVp9PictureInfo() { check(sceVdecswResetDecoder(session.decoder) == 0, "VP9 reset failed"); check(throws([&] { sceVdecswGetVp9PictureInfo(&outputs[0], &info); }) && sameBytes(info, unchanged), "VP9 picture info survived a decoder reset"); + check(throws([&] { sceVdecswGetPictureInfo(&outputs[0], &info, nullptr); }) && sameBytes(info, unchanged), "generic picture info survived a decoder reset"); } void testVp9PictureInfoOfAvcPicture() { @@ -416,11 +510,14 @@ void testHevcMain10() { check(sceVdecswSyncDecodeOutput(session.decoder, &output) == 0 && output.isValid, "the Main 10 HEVC picture was not output"); HevcPictureInfo info{sizeof(HevcPictureInfo)}; check(sceVdecswGetHevcPictureInfo(&output, &info) == 0 && info.isValid, "Main 10 HEVC picture info missing"); + checkGenericPictureInfo(output, info); check(info.generalProfileIdc == 2 && info.bitDepthLumaMinus8 == 2 && info.bitDepthChromaMinus8 == 2, "Main 10 HEVC profile or bit depth differs"); } int main() { try { + testGenericPictureInfoValidation(); + testGenericPictureInfoLifetime(); testDecode(); testHevc(); testHevcCrop(); diff --git a/core/libs/tests/GuestVectorAlgorithms.cpp b/core/libs/tests/GuestVectorAlgorithms.cpp index f596b715b6..99eaebf937 100644 --- a/core/libs/tests/GuestVectorAlgorithms.cpp +++ b/core/libs/tests/GuestVectorAlgorithms.cpp @@ -5,8 +5,43 @@ #include #include + static void Require(bool value) { if (!value) std::abort(); } +static void CheckSizedHelpers() { + const std::uint8_t bytes[] = {3, 0xff, 3, 9, 0xff}; + Require(__std_find_trivial_1_nid_postfix(bytes, bytes + 5, 0xff) == bytes + 1); + Require(__std_find_trivial_1_nid_postfix(bytes, bytes + 5, 4) == bytes + 5); + Require(__std_find_trivial_1_nid_postfix(bytes, bytes, 3) == bytes); + Require(__std_count_trivial_1_nid_postfix(bytes, bytes + 5, 3) == 2 && __std_count_trivial_1_nid_postfix(bytes, bytes + 5, 7) == 0); + Require(__std_max_element_1_nid_postfix(bytes, bytes + 5, false) == bytes + 1); + Require(__std_max_element_1_nid_postfix(bytes, bytes + 5, true) == bytes + 3); + + const std::uint16_t halves[] = {0x8000, 5, 0x7fff, 5, 0x8000}; + Require(__std_count_trivial_2_nid_postfix(halves, halves + 5, 5) == 2 && __std_count_trivial_2_nid_postfix(halves, halves, 5) == 0); + Require(__std_min_element_2_nid_postfix(halves, halves + 5, false) == halves + 1); + Require(__std_min_element_2_nid_postfix(halves, halves + 5, true) == halves); + Require(__std_max_element_2_nid_postfix(halves, halves + 5, false) == halves); + Require(__std_max_element_2_nid_postfix(halves, halves + 5, true) == halves + 2); + + const std::uint32_t words[] = {7, 0xfffffff0u, 1, 1, 0xfffffff0u}; + Require(__std_find_trivial_4_nid_postfix(words, words + 5, 1) == words + 2); + Require(__std_find_trivial_4_nid_postfix(words, words + 5, 2) == words + 5); + Require(__std_count_trivial_4_nid_postfix(words, words + 5, 0xfffffff0u) == 2); + Require(__std_min_element_4_nid_postfix(words, words + 5, false) == words + 2); + Require(__std_min_element_4_nid_postfix(words, words + 5, true) == words + 1); + Require(__std_min_element_4_nid_postfix(words, words, true) == words); + + const std::uint64_t wides[] = {0x8000000000000000ull, 2, 0x7fffffffffffffffull, 0x8000000000000000ull}; + Require(__std_find_trivial_8_nid_postfix(wides, wides + 4, 0x8000000000000000ull) == wides); + Require(__std_find_trivial_8_nid_postfix(wides + 1, wides + 4, 0x8000000000000000ull) == wides + 3); + Require(__std_min_element_8_nid_postfix(wides, wides + 4, true) == wides); + Require(__std_min_element_8_nid_postfix(wides, wides + 4, false) == wides + 1); + Require(__std_max_element_8_nid_postfix(wides, wides + 4, false) == wides); + Require(__std_max_element_8_nid_postfix(wides, wides + 4, true) == wides + 2); + Require(__std_max_element_8_nid_postfix(wides, wides, false) == wides); +} + static void CheckFindAndCount() { const std::uint16_t halves[] = {5, 7, 9, 7}; Require(__std_find_trivial_2_nid_postfix(halves, halves + 4, 7) == halves + 1); @@ -103,6 +138,7 @@ static void CheckReverseAndSwap() { } int main() { + CheckSizedHelpers(); CheckFindAndCount(); CheckUnsizedRescan(); CheckMinMax(); diff --git a/core/libs/tests/GuestVideoOutResolutionStatus.cpp b/core/libs/tests/GuestVideoOutResolutionStatus.cpp index 338783ec96..97f0a119a0 100644 --- a/core/libs/tests/GuestVideoOutResolutionStatus.cpp +++ b/core/libs/tests/GuestVideoOutResolutionStatus.cpp @@ -61,6 +61,7 @@ int main() { for (const auto byte : vrr) Require(byte == std::byte{0xff}); Require(Rejects(handle, nullptr)); + Require(Rejects(-1, nullptr)); Require(Rejects(0, &status)); Require(Rejects(-1, &status)); Require(Rejects(NEVER_OPENED_HANDLE, &status)); diff --git a/core/libs/tests/GuestVoiceChat.cpp b/core/libs/tests/GuestVoiceChat.cpp index 0b80ea67e8..c9390b63dd 100644 --- a/core/libs/tests/GuestVoiceChat.cpp +++ b/core/libs/tests/GuestVoiceChat.cpp @@ -22,8 +22,8 @@ int main() { Require(sceVoiceChatInitialize(nullptr) == 0, "initialization failed"); Require(sceVoiceChatProcessEvent() == 0, "event processing failed"); - Require(sceVoiceChatRegisterHandlers(nullptr) == 0, "register handlers failed"); - Require(sceVoiceChatRegisterMicEventHandler(nullptr, nullptr) == 0, "register mic event handler failed"); + Require(sceVoiceChatRegisterHandlers(nullptr, nullptr) == 0, "register handlers failed"); + Require(sceVoiceChatRegisterMicEventHandler(0, nullptr, nullptr) == 0, "register mic event handler failed"); Require(sceVoiceChatRegisterChannelEventHandler(nullptr, nullptr) == 0, "register channel event handler failed"); Require(sceVoiceChatUnregisterMicEventHandler(nullptr) == 0, "unregister mic event handler failed"); Require(sceVoiceChatStartChannelEvent(nullptr) == 0, "start channel event failed"); diff --git a/core/libs/tests/GuestVrServiceDialog.cpp b/core/libs/tests/GuestVrServiceDialog.cpp index 95f074d84c..5b7433ce53 100644 --- a/core/libs/tests/GuestVrServiceDialog.cpp +++ b/core/libs/tests/GuestVrServiceDialog.cpp @@ -23,8 +23,10 @@ int main() { std::int32_t result[2] = {-1, -1}; Require(sceVrServiceDialogTerminate() == kErrNotInitialized); + Require(sceVrServiceDialogOpen(nullptr) == kErrNotInitialized); Require(sceVrServiceDialogOpen(¶m) == kErrNotInitialized); Require(sceVrServiceDialogClose() == kErrNotInitialized); + Require(sceVrServiceDialogGetResult(nullptr) == kErrNotInitialized); Require(sceVrServiceDialogGetResult(result) == kErrNotInitialized); Require(sceVrServiceDialogGetStatus() == kStatusNone); Require(sceVrServiceDialogUpdateStatus() == kStatusNone); diff --git a/core/libs/tests/GuestVrTracker2.cpp b/core/libs/tests/GuestVrTracker2.cpp index 461c90bca7..a1e4b0693a 100644 --- a/core/libs/tests/GuestVrTracker2.cpp +++ b/core/libs/tests/GuestVrTracker2.cpp @@ -1,17 +1,21 @@ #include "prx/libSceVrTracker2/include/Api.hpp" -#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/libkernel.hpp" #include "ProcessParametersFixture.hpp" +#include #include #include #include #include #include +#include +#include +#include #include +#include +#include -extern "C" { - -} +static_assert(sizeof(VrTracker2InitializeParameter) == 72); namespace { @@ -19,6 +23,8 @@ alignas(16) std::array process{}; constexpr std::int32_t NotInitialized = static_cast(0x8A740001u); constexpr std::int32_t InvalidArgument = static_cast(0x8A740006u); +constexpr std::uint32_t TrackerMemory = 32u << 20; +constexpr std::size_t WorkMemoryTail = 64u << 10; void Require(bool condition, const char* message) { if (!condition) { @@ -49,6 +55,118 @@ bool Throws(TCall call) { return false; } +VrTracker2InitializeParameter TrackerParameter(std::vector& memory) { + VrTracker2InitializeParameter parameter{}; + parameter.size = sizeof(parameter); + parameter.threadPriorities[0] = 256; + parameter.threadPriorities[1] = 767; + parameter.threadAffinityMasks[0] = 0x1fff; + parameter.workMemory = memory.data(); + parameter.workMemorySize = static_cast(memory.size()); + parameter.workMemoryAlignment = 0x10000; + return parameter; +} + +bool WorkMemoryCleared(const std::vector& memory) { + return AllEqual(memory.data(), memory.data() + TrackerMemory, 0) && AllEqual(memory.data() + TrackerMemory, memory.data() + memory.size(), 0xAA); +} + +bool WorkMemoryUntouched(const std::vector& memory) { + return AllEqual(memory.data(), memory.data() + memory.size(), 0xAA); +} + +std::int32_t InitializeFresh(std::vector& memory, const VrTracker2InitializeParameter& parameter) { + std::fill(memory.begin(), memory.end(), 0xAA); + return sceVrTracker2Initialize(¶meter); +} + +void CheckInitializeValidation(std::vector& memory) { + const auto valid = TrackerParameter(memory); + Require(sceVrTracker2Initialize(nullptr) == InvalidArgument, "initialize null"); + for (const std::uint32_t size : {0u, 71u, 73u}) { + auto parameter = valid; + parameter.size = size; + parameter.mode = 1000; + Require(InitializeFresh(memory, parameter) == InvalidArgument, "initialize size before mode"); + } + for (const std::uint32_t mode : {1u, 999u, 1002u, 0xffffffffu}) { + auto parameter = valid; + parameter.mode = mode; + Require(InitializeFresh(memory, parameter) == InvalidArgument, "initialize mode"); + } + auto parameter = valid; + parameter.workMemory = nullptr; + Require(InitializeFresh(memory, parameter) == InvalidArgument, "initialize work memory null"); + parameter = valid; + parameter.workMemorySize = TrackerMemory - 1; + Require(InitializeFresh(memory, parameter) == InvalidArgument, "initialize work memory size"); + parameter = valid; + parameter.workMemoryAlignment = 0xffff; + Require(InitializeFresh(memory, parameter) == InvalidArgument, "initialize alignment"); + for (std::size_t index = 0; index < sizeof(parameter.reserved); ++index) { + parameter = valid; + parameter.reserved[index] = 1; + Require(InitializeFresh(memory, parameter) == InvalidArgument, "initialize reserved byte"); + } + for (std::size_t index = 0; index < 2; ++index) { + for (const std::int32_t priority : {255, 768, -1}) { + parameter = valid; + parameter.threadPriorities[index] = priority; + Require(InitializeFresh(memory, parameter) == InvalidArgument, "initialize thread priority"); + } + for (const std::uint64_t mask : {std::uint64_t{0x2000}, std::uint64_t{0x3fff}, std::uint64_t{1} << 63}) { + parameter = valid; + parameter.threadAffinityMasks[index] = mask; + Require(InitializeFresh(memory, parameter) == InvalidArgument, "initialize affinity mask"); + } + } + Require(WorkMemoryUntouched(memory), "initialize validation leaves work memory"); +} + +void CheckInitializeTracking(std::vector& memory, std::uint32_t sdkVersion) { + const auto valid = TrackerParameter(memory); + Require(InitializeFresh(memory, valid) == -1, "initialize tracking start result"); + Require(WorkMemoryCleared(memory), "initialize clears tracker memory"); + Require(sceVrTracker2Finalize() == NotInitialized, "initialize failure leaves module uninitialized"); + Require(VrTracker2NotInitialized() == NotInitialized, "initialize failure keeps uninitialized answers"); + Require(InitializeFresh(memory, valid) == -1, "initialize retry after failure"); + auto parameter = valid; + parameter.workMemoryAlignment = 0x10001; + parameter.option = 0xffffffffu; + parameter.coordinateSystem = 2; + parameter.disableOption = 1; + parameter.threadAffinityMasks[1] = 0x1fff; + Require(InitializeFresh(memory, parameter) == -1, "initialize late checks follow tracking"); + for (const std::int8_t optionMode : {std::int8_t{-1}, std::int8_t{0}}) { + parameter = valid; + parameter.optionMode = optionMode; + Require(InitializeFresh(memory, parameter) == -1, "initialize accepted option mode"); + } + for (const std::int8_t optionMode : {std::int8_t{2}, std::int8_t{-2}, std::int8_t{127}}) { + parameter = valid; + parameter.optionMode = optionMode; + Require(InitializeFresh(memory, parameter) == InvalidArgument, "initialize option mode"); + Require(WorkMemoryCleared(memory), "initialize option mode follows clearing"); + } + parameter = valid; + parameter.optionMode = 1; + Require(InitializeFresh(memory, parameter) == (sdkVersion >= 0x7000000 ? -1 : InvalidArgument), "initialize SDK option mode"); + Require(sceVrTracker2Finalize() == NotInitialized, "initialize leaves module uninitialized"); +} + +std::filesystem::path EnterApplicationRoot() { + std::random_device device; + std::filesystem::path root; + do { + root = std::filesystem::temp_directory_path() / + ("guest_vr_tracker2_" + std::to_string(getpid_nid_postfix()) + "_" + std::to_string(device())); + } while (!std::filesystem::create_directory(root)); + std::filesystem::create_directories(root / "app0" / "sce_sys"); + std::ofstream(root / "app0" / "sce_sys" / "param.json") << "{\n \"contentId\": \"IV0000-ABCD00000_00-VRTRACKER2000000\",\n \"localizedParameters\": {\"defaultLanguage\": \"en-US\", \"en-US\": {\"titleName\": \"VrTracker2\"}},\n \"titleId\": \"ABCD00000\"\n}\n"; + std::filesystem::current_path(root); + return root; +} + } int main() { @@ -129,6 +247,15 @@ int main() { parameter[0] = 0x21; Require(sceVrTracker2QueryMemory(reinterpret_cast(parameter.data()), reinterpret_cast(requirement.data())) == InvalidArgument, "query size"); - Require(Throws([] { sceVrTracker2Initialize(nullptr); }), "unknown export"); + std::vector memory(TrackerMemory + WorkMemoryTail); + CheckInitializeValidation(memory); + const auto previous = std::filesystem::current_path(); + const auto root = EnterApplicationRoot(); + for (const std::uint32_t version : {0x6ffffffu, 0x7000000u}) { + std::memcpy(process.data() + 0x10, &version, sizeof(version)); + CheckInitializeTracking(memory, version); + } + std::filesystem::current_path(previous); + std::filesystem::remove_all(root); return 0; } diff --git a/core/libs/tests/GuestWideConversion.cpp b/core/libs/tests/GuestWideConversion.cpp index 5590c6124a..f1971d9e13 100644 --- a/core/libs/tests/GuestWideConversion.cpp +++ b/core/libs/tests/GuestWideConversion.cpp @@ -153,6 +153,17 @@ static void CheckUnsizedScan() { Require(swscanf_nid_postfix(u" 42 -7", u"%d%d", &first, &second) == 2 && first == 42 && second == -7, "swscanf %d"); Require(swscanf_nid_postfix(u"x=0x1f", u"x=%i", &first) == 1 && first == 31, "swscanf %i with a hex prefix"); Require(swscanf_nid_postfix(u"1 2", u"%*d%d", &first) == 1 && first == 2, "swscanf suppressed %d"); + float scannedFloat = 0; + double scannedDouble = 0; + long double scannedLongDouble = 0; + errno = 0; + Require(swscanf_nid_postfix(u"1e39 1e400", u"%f%lf", &scannedFloat, &scannedDouble) == 2 && scannedFloat == std::numeric_limits::infinity() && + scannedDouble == std::numeric_limits::infinity() && errno == 0, "swscanf leaves errno alone for an overflowing float and double"); + Require(swscanf_nid_postfix(u"1e-400", u"%lf", &scannedDouble) == 1 && scannedDouble == 0 && errno == 0, "swscanf leaves errno alone for an underflowing double"); + errno = 0; + Require(swscanf_nid_postfix(u"1e-5000", u"%Lf", &scannedLongDouble) == 1 && scannedLongDouble == 0 && errno == ERANGE, "swscanf sets ERANGE for an out-of-range long double"); + errno = 0; + Require(swscanf_nid_postfix(u"1e-50", u"%e", &scannedFloat) == 1 && scannedFloat == 0 && errno == 0, "swscanf leaves errno alone for an underflowing float"); Require(swscanf_nid_postfix(u"12345", u"%3d%d", &first, &second) == 2 && first == 123 && second == 45, "swscanf %d width"); std::int64_t wide = 0; Require(swscanf_nid_postfix(u"5000000000", u"%ld", &wide) == 1 && wide == 5000000000LL, "swscanf %ld is 64-bit"); diff --git a/core/libs/tests/GuestWideFormatting.cpp b/core/libs/tests/GuestWideFormatting.cpp index 56b278d8ae..09fb72b4c8 100644 --- a/core/libs/tests/GuestWideFormatting.cpp +++ b/core/libs/tests/GuestWideFormatting.cpp @@ -2,6 +2,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/GuestTime.hpp" +#include #include #include #include @@ -177,6 +178,19 @@ static void CheckSecureWideBoundary() { "vsnwprintf_s oversized size leaves the buffer untouched"); } +static void CheckFloat() { + char16_t buffer[64]; + Require(swprintf_nid_postfix(buffer, 64, u"[%.0f|%.1f|%#.3g|%.20f]", 2.5, 0.25, 2025.0, 0.1) == 39 && + buffer == std::u16string(u"[3|0.3|2.03e+03|0.10000000000000000555]"), "swprintf rounds and caps decimal digits"); + Require(swprintf_nid_postfix(buffer, 64, u"[%.0f|%-8.2e]", 67108865.5, 1125.0) == 19 && buffer == std::u16string(u"[67108865|1.13e+03]"), + "swprintf rounds a large tie down"); + Require(swprintf_nid_postfix(buffer, 64, u"[%a|%.3a]", 0x1p-1074, 0x0.fffffffffffffp-1022) == 25 && + buffer == std::u16string(u"[0x1p-1074|0x2.000p-1023]"), "swprintf normalises subnormal hexadecimal floats"); + Require(swprintf_nid_postfix(buffer, 64, u"[%.2Lf]", 0.125L) == 6 && buffer == std::u16string(u"[0.13]"), "swprintf long double rounding"); + Require(swprintf_nid_postfix(buffer, 64, u"[%La|%.0La]", 0.1L, 1.5L) == 32 && buffer == std::u16string(u"[0x1.999999999999999ap-4|0x2p+0]"), + "swprintf long double hexadecimal"); +} + static void CheckSwprintf() { char16_t buffer[8]; for (auto& unit : buffer) unit = u'x'; @@ -295,13 +309,23 @@ static void CheckAlternateOctalZero() { Require(snwprintf_s_nid_postfix(buffer, 1, u"%#.0o", 0u) == 1 && buffer[0] == 0 && buffer[1] == u'y', "octal zero bounded output"); } +static void CheckNanSign() { + char16_t buffer[64]; + Require(swprintf_nid_postfix(buffer, 64, u"[%f][%+f][% F][%5.1e]", -NAN, -NAN, NAN, -NAN) == 25, "NaN sign length"); + Require(buffer == std::u16string(u"[-nan][-nan][ NAN][ -nan]"), "NaN sign"); + Require(swprintf_nid_postfix(buffer, 64, u"[%-6g][%06a][%LG][%+f][%+G]", NAN, -NAN, -static_cast(NAN), -INFINITY, NAN) == 34, "NaN flags length"); + Require(buffer == std::u16string(u"[nan ][ -nan][-NAN][-inf][+NAN]"), "NaN flags"); +} + int main() { CheckAlternateOctalZero(); CheckBounded(); + CheckNanSign(); CheckPositional(); CheckPointer(); CheckCount(); CheckSwprintf(); + CheckFloat(); CheckSecureWideBoundary(); CheckWcsftime(); Check(u"%.2s", "\xc3\xa9\xc3\xa8", u"\u00e9\u00e8"); diff --git a/core/libs/tests/GuestWindowsDirectoryLinkTypes.cpp b/core/libs/tests/GuestWindowsDirectoryLinkTypes.cpp new file mode 100644 index 0000000000..1c5227b3f8 --- /dev/null +++ b/core/libs/tests/GuestWindowsDirectoryLinkTypes.cpp @@ -0,0 +1,197 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Directory link type check failed at line %d\n", line); + std::exit(1); + } +} + +#define Require(value) Check((value), __LINE__) + +static void Junction(const std::filesystem::path& link, const std::filesystem::path& target) { + Require(std::filesystem::create_directory(link)); + 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{}; + Require(substituteName.size() + printName.size() + 2 <= std::size(data.names)); + 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 auto handle = CreateFileW(link.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(handle != INVALID_HANDLE_VALUE); + DWORD returned = 0; + const auto result = DeviceIoControl(handle, FSCTL_SET_REPARSE_POINT, &data, 8 + data.dataLength, nullptr, 0, &returned, nullptr); + Require(CloseHandle(handle) != 0); + Require(result != 0); +} + +static void OutsideApp0() { + const auto root = std::filesystem::absolute("anyps5-directory-links-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + Require(std::filesystem::create_directory(root / "directory")); + { std::ofstream file(root / "regular"); file << "data"; } + std::vector links{"junction", "dangling-junction", "loop-junction"}; + Junction(root / links[0], root / "directory"); + Junction(root / links[1], root / "missing"); + Junction(root / links[2], root / links[2]); + if (CreateSymbolicLinkW((root / "file-link").c_str(), L"regular", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE)) { + links.push_back("file-link"); + Require(CreateSymbolicLinkW((root / "directory-link").c_str(), L"directory", + SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + links.push_back("directory-link"); + Require(CreateSymbolicLinkW((root / "dangling-link").c_str(), L"missing", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + links.push_back("dangling-link"); + } else { + Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD); + std::fprintf(stderr, "Symbolic-link cases require Windows developer mode or symbolic-link privilege; junction cases still run\n"); + } + const int descriptor = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(descriptor >= 0); + std::map entries; + std::array buffer{}; + for (;;) { + const int bytes = sceKernelGetdents(descriptor, buffer.data(), static_cast(buffer.size())); + Require(bytes >= 0 && bytes <= static_cast(buffer.size())); + if (bytes == 0) break; + for (int offset = 0; offset < bytes;) { + std::uint16_t length = 0; + std::memcpy(&length, buffer.data() + offset + 4, sizeof(length)); + const auto nameLength = static_cast(buffer[offset + 7]); + Require(length >= 8 + nameLength + 1 && offset + length <= bytes); + const std::string name(buffer.data() + offset + 8, nameLength); + Require(entries.emplace(name, static_cast(buffer[offset + 6])).second); + offset += length; + } + } + Require(sceKernelClose(descriptor) == 0); + Require(entries.size() == links.size() + 4); + Require(entries.at(".") == 4 && entries.at("..") == 4); + Require(entries.at("directory") == 4 && entries.at("regular") == 8); + for (const auto& name : links) { + if (entries.at(name) != 10) + std::fprintf(stderr, "%s: getdents type %u, expected DT_LNK (10)\n", name.c_str(), entries.at(name)); + Require(entries.at(name) == 10); + const auto path = root / name; + FileStat status{}; + Require(lstat_nid_postfix(path.string().c_str(), &status) == 0); + Require((status.st_mode & 0170000) == 0120000); + } + for (const auto& name : links) Require(RemoveDirectoryW((root / name).c_str()) != 0 || DeleteFileW((root / name).c_str()) != 0); + Require(std::filesystem::remove_all(root) == 3); +} + + +static std::map Types(const char* path) { + const int descriptor = sceKernelOpen(path, SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(descriptor >= 0); + std::map entries; + std::array buffer{}; + for (;;) { + const int bytes = sceKernelGetdents(descriptor, buffer.data(), static_cast(buffer.size())); + Require(bytes >= 0 && bytes <= static_cast(buffer.size())); + if (bytes == 0) break; + for (int offset = 0; offset < bytes;) { + std::uint16_t length = 0; + std::memcpy(&length, buffer.data() + offset + 4, sizeof(length)); + const auto nameLength = static_cast(buffer[offset + 7]); + Require(length >= 8 + nameLength + 1 && offset + length <= bytes); + Require(entries.emplace(std::string(buffer.data() + offset + 8, nameLength), + static_cast(buffer[offset + 6])).second); + offset += length; + } + } + Require(sceKernelClose(descriptor) == 0); + return entries; +} + +int main() { + const auto previous = std::filesystem::current_path(); + const auto root = previous / ("anyps5-app0-links-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directories(root / "app0" / "real")); + Require(std::filesystem::create_directories(root / "external")); + Require(std::filesystem::create_directories(root / "data")); + Require(std::filesystem::create_directories(root / "app0-sibling")); + std::filesystem::current_path(root); + OutsideApp0(); + std::vector links; + const auto junction = [&](const std::filesystem::path& link, const std::filesystem::path& target) { + Junction(root / link, root / target); + links.push_back(link); + }; + junction("app0/packed", "external"); + junction("app0/chain", "app0/packed"); + junction("app0/dangling", "missing"); + junction("app0/loop", "app0/loop"); + junction("app0/real/nested", "external"); + junction("external/nested", "app0/real"); + junction("data/saved", "external"); + junction("app0-sibling/saved", "external"); + { std::ofstream file(root / "external/regular"); file << "data"; } + const bool symbolic = CreateSymbolicLinkW((root / "app0/file-link").c_str(), L"..\\external\\regular", + SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0; + if (symbolic) { + links.push_back("app0/file-link"); + Require(CreateSymbolicLinkW((root / "app0/relative").c_str(), L"..\\external", + SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + links.push_back("app0/relative"); + } else { + Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD); + std::fprintf(stderr, "app0 symbolic-link subcases skipped: privilege unavailable\n"); + } + auto app0 = Types("/app0"); + std::fprintf(stderr, "app0 packed/chain/real types: %u/%u/%u (expected 4/4/4)\n", app0.at("packed"), app0.at("chain"), app0.at("real")); + Require(app0.at("real") == 4 && app0.at("packed") == 4 && app0.at("chain") == 4); + Require(app0.at("dangling") == 10 && app0.at("loop") == 10); + if (symbolic) Require(app0.at("file-link") == 8 && app0.at("relative") == 4); + Require(Types("/app0/real").at("nested") == 4); + Require(Types("/app0/packed").at("nested") == 10); + Require(Types("/data").at("saved") == 10); + Require(Types((root / "app0-sibling").string().c_str()).at("saved") == 10); + FileStat status{}; + Require(lstat_nid_postfix("/app0/packed", &status) == 0 && (status.st_mode & 0170000) == 0120000); + for (auto it = links.rbegin(); it != links.rend(); ++it) Require(RemoveDirectoryW((root / *it).c_str()) != 0 || DeleteFileW((root / *it).c_str()) != 0); + std::filesystem::rename(root / "app0", root / "application"); + Junction(root / "app0", root / "application"); + Junction(root / "application/packed", root / "external"); + Require(Types("/app0").at("packed") == 4); + Require(std::filesystem::remove(root / "application/packed")); + Require(std::filesystem::remove(root / "app0")); + std::filesystem::current_path(previous); + Require(root.parent_path() == previous && root.filename().string().starts_with("anyps5-app0-links-")); + std::filesystem::remove_all(root); + std::puts("Windows directory link type tests passed"); +} diff --git a/core/libs/tests/ImageSampleGather.cpp b/core/libs/tests/ImageSampleGather.cpp index 3d3b40d608..2182e4d5cd 100644 --- a/core/libs/tests/ImageSampleGather.cpp +++ b/core/libs/tests/ImageSampleGather.cpp @@ -62,6 +62,21 @@ static void CheckPckRejectsZeroMask(std::uint32_t encoding) { } Require(rejected); } +static void CheckGatherCompareAdjust(std::uint32_t word0, std::uint32_t opcodeId, RdnaOpcode opcode, std::uint32_t flags, std::uint32_t addressComponents) { + const std::array code{word0, 0x00801e33u}; + const RdnaInstruction instruction = DecodeRdnaMimg(0u, code, 0u); + Require(instruction.op == opcode); + Require(instruction.imageOpcodeId == opcodeId); + Require(instruction.imageSampleFlags == flags); + Require(instruction.imageAddressComponents == addressComponents); + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + TranslationContext context(program, block, 256); + context.TranslateInstruction(instruction); + Require(program.Resources().memoryInfo.size() == 1u); + Require(program.Resources().memoryInfo.back().imageSampleFlags == flags); +} static void CheckUnsupportedOpcodeDiagnostic(std::uint32_t encoding, std::string_view expected) { const std::array code{(0x3cu << 26u) | ((encoding & 0x7fu) << 18u) | (encoding >> 7u), 0u}; try { @@ -142,6 +157,8 @@ int main() { CheckPck(0x63u, RdnaOpcode::ImageGather8hPck, 0xFu, 4u); CheckPckRejectsZeroMask(0x62u); CheckPckRejectsZeroMask(0x63u); + CheckGatherCompareAdjust(0xf1200109u, 0xc8u, RdnaOpcode::ImageGather4C, RdnaImageSampleFlagCompare | RdnaImageSampleFlagAdjust, 3u); + CheckGatherCompareAdjust(0xf1600109u, 0xd8u, RdnaOpcode::ImageGather4CO, RdnaImageSampleFlagCompare | RdnaImageSampleFlagOffset | RdnaImageSampleFlagAdjust, 4u); CheckUnsupportedOpcodeDiagnostic(0x06u, "MIMG opcode 0x06"); CheckUnsupportedOpcodeDiagnostic(0xffu, "MIMG opcode 0xff"); } diff --git a/core/libs/tests/JobManagerFailures.cpp b/core/libs/tests/JobManagerFailures.cpp new file mode 100644 index 0000000000..d6586fcb9b --- /dev/null +++ b/core/libs/tests/JobManagerFailures.cpp @@ -0,0 +1,156 @@ +#include "prx/libSceJobManager/libSceJobManager.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +int failure = 0; +int calls = 0; +int attributes = 0; +int threads = 0; +int queues = 0; +int registrations = 0; + +bool Fail() { return ++calls == failure; } + +void Check(bool condition) { + if (!condition) std::abort(); +} + +struct EventQueue { + std::mutex lock; + std::condition_variable ready; + bool triggered = false; +}; + +} + +extern "C" { + +int APS5_VABI scePthreadAttrInit(PthreadAttr* value) { + if (Fail()) return 12; + *value = reinterpret_cast(new int); + ++attributes; + return 0; +} + +int APS5_VABI scePthreadAttrDestroy(PthreadAttr* value) { + delete reinterpret_cast(*value); + *value = nullptr; + --attributes; + return 0; +} + +int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr*, int) { return Fail() ? 22 : 0; } +int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr*, const KernelSchedParam*) { return Fail() ? 22 : 0; } +int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr*, std::size_t) { return Fail() ? 22 : 0; } +int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr*, KernelCpumask) { return Fail() ? 22 : 0; } + +int APS5_VABI scePthreadCreate(Pthread* value, const PthreadAttr*, PthreadEntry entry, void* argument, const char*) { + if (Fail()) return 11; + *value = reinterpret_cast(new std::thread([=] { entry(argument); })); + ++threads; + return 0; +} + +int APS5_VABI scePthreadJoin(Pthread value, void**) { + auto* thread = reinterpret_cast(value); + thread->join(); + delete thread; + --threads; + return 0; +} + +int APS5_VABI sceKernelCreateEqueue(KernelEqueue* value, const char*) { + if (Fail()) return 12; + *value = reinterpret_cast(new EventQueue); + ++queues; + return 0; +} + +int APS5_VABI sceKernelDeleteEqueue(KernelEqueue value) { + delete reinterpret_cast(value); + --queues; + return 0; +} + +int APS5_VABI sceKernelWaitEqueue(KernelEqueue value, KernelEvent* event, int, int* count, const KernelUseconds*) { + auto& queue = *reinterpret_cast(value); + std::unique_lock guard(queue.lock); + queue.ready.wait(guard, [&queue] { return queue.triggered; }); + event->filter = -11; + event->ident = 0; + *count = 1; + return 0; +} + +int APS5_VABI sceKernelAddUserEventEdge(KernelEqueue, int) { return 0; } + +int APS5_VABI sceKernelTriggerUserEvent(KernelEqueue value, int, void*) { + auto& queue = *reinterpret_cast(value); + const std::lock_guard guard(queue.lock); + queue.triggered = true; + queue.ready.notify_all(); + return 0; +} + +int APS5_VABI sceKernelGetEventFilter(const KernelEvent* event) { return event->filter; } +std::uintptr_t APS5_VABI sceKernelGetEventId(const KernelEvent* event) { return event->ident; } + +int APS5_VABI sceAgcDriverAddEqEvent(KernelEqueue, int, void*) { + if (Fail()) return 22; + ++registrations; + return 0; +} + +int APS5_VABI sceAgcDriverDeleteEqEvent(KernelEqueue, int) { + --registrations; + return 0; +} + + +} + +int main() { + constexpr std::size_t size = 1024 * 1024; + void* memory = ::operator new(size, std::align_val_t{256}); + for (const std::uint32_t computeSequences : {0u, 2u}) { + int failures = 0; + for (failure = 1; failure < 40; ++failure) { + alignas(64) unsigned char manager[0x100]{}; + _ZN3sce3Job10JobManagerC1Ev(manager); + calls = 0; + bool threw = false; + try { + Check(_ZN3sce3Job10JobManager10initializeEPvmjjjmPKNS1_12ConfigParamsE(manager, memory, size, 3, 2, computeSequences, computeSequences ? 5 : 0, nullptr) == 0); + } catch (const std::runtime_error&) { + threw = true; + ++failures; + } + if (threw) { + Check(_ZN3sce3Job10JobManager27getSequenceFactoryInterfaceENS1_8PriorityE(manager, 0) == nullptr); + Check(_ZN3sce3Job10JobManager8shutdownEv(manager) == static_cast(0x81581010u)); + } else { + Check(_ZN3sce3Job10JobManager8shutdownEv(manager) == 0); + } + Check(attributes == 0 && threads == 0 && queues == 0 && registrations == 0); + if (!threw) break; + const int injected = failure; + failure = 0; + Check(_ZN3sce3Job10JobManager10initializeEPvmjjjmPKNS1_12ConfigParamsE(manager, memory, size, 3, 2, computeSequences, computeSequences ? 5 : 0, nullptr) == 0); + Check(_ZN3sce3Job10JobManager8shutdownEv(manager) == 0); + Check(attributes == 0 && threads == 0 && queues == 0 && registrations == 0); + failure = injected; + } + Check(failures == (computeSequences ? 27 : 18)); + std::printf("initialization failures recovered: %d\n", failures); + } + ::operator delete(memory, std::align_val_t{256}); +} diff --git a/core/libs/tests/JobManagerProfiler.cpp b/core/libs/tests/JobManagerProfiler.cpp new file mode 100644 index 0000000000..a2a4bebb5e --- /dev/null +++ b/core/libs/tests/JobManagerProfiler.cpp @@ -0,0 +1,394 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceJobManager/libSceJobManager.hpp" +#include "prx/libSceAgcDriver/libSceAgcDriver.hpp" +#include "prx/libSceAgc/libSceAgc.hpp" +#include "SceTypes.hpp" +#include "prx/libSceAgcDriver/tests/execution/VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace { + +enum class Kind { Sequence, Job, Dispatch, Body }; + +struct Event { + Kind kind; + const char* name; + std::uint16_t manager; + std::uint16_t sequence; + std::uint16_t dispatch; + std::uint16_t value; + std::uint16_t event; + std::uint64_t entry; + std::uint64_t argument; +}; + +std::mutex eventLock; +std::vector events; + +void Check(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "job manager profiler check failed at line %d\n", line); + std::abort(); + } +} + +#define Require(value) Check((value), __LINE__) + +void Record(Event event) { + const std::lock_guard guard(eventLock); + events.push_back(event); +} + +std::vector Snapshot() { + const std::lock_guard guard(eventLock); + return events; +} + +void Clear() { + const std::lock_guard guard(eventLock); + events.clear(); +} + +struct Fixture { + alignas(256) std::array memory{}; + alignas(64) std::array object{}; + ISequenceFactory* factory = nullptr; + std::uint16_t id = 0; + + explicit Fixture(std::uint32_t workers = 1) { + _ZN3sce3Job10JobManagerC1Ev(object.data()); + Require(_ZN3sce3Job10JobManager10initializeEPvmjjjmPKNS1_12ConfigParamsE(object.data(), memory.data(), memory.size(), workers, 2, 2, 0x122, nullptr) == 0); + factory = _ZN3sce3Job10JobManager34getUnifiedSequenceFactoryInterfaceENS1_8PriorityE(object.data(), 0); + id = static_cast(_mm_crc32_u64(0, reinterpret_cast(memory.data())) >> 16); + Clear(); + } + + void Stop() { + Require(_ZN3sce3Job10JobManager8shutdownEv(object.data()) == 0); + } + + ISequence* Create(const char* name, bool compute = false) { + ISequence* sequence = nullptr; + if (compute) Require(factory->CreateCompute(&sequence, 16, 8, 8, 4, name) == 0); + else Require(factory->Create(&sequence, 16, 8, 8, 4, name) == 0); + return sequence; + } +}; + +struct Context { + std::atomic entered{false}; + std::atomic calls{0}; + std::atomic released{true}; +}; + +void APS5_VABI Job(void* argument, std::uint32_t index) { + Record({Kind::Body, nullptr, 0, 0, 0, static_cast(index), 0, 0, reinterpret_cast(argument)}); + auto& context = *static_cast(argument); + ++context.calls; + context.entered.store(true); + while (!context.released.load()) std::this_thread::yield(); +} + +void Await(const std::atomic& flag) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (!flag.load() && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + Require(flag.load()); +} + +struct Reader { + std::vector records = Snapshot(); + std::size_t next = 0; + std::uint16_t manager; + + explicit Reader(std::uint16_t id) : manager(id) {} + + Event Take(Kind kind) { + Require(next < records.size()); + const Event result = records[next++]; + Require(result.kind == kind); + if (kind != Kind::Body) Require(result.manager == manager); + return result; + } + + void Sequence(const char* name, std::uint16_t id, std::uint16_t event) { + const Event result = Take(Kind::Sequence); + Require(result.name == name && result.sequence == id && result.event == event); + } + + std::uint16_t Dispatch(const char* name, Context& context, std::uint16_t count, std::uint16_t sequence = 0) { + std::uint16_t dispatch = 0; + for (std::uint16_t index = 0; index < count; ++index) { + const Event begin = Take(Kind::Job); + if (index == 0) dispatch = begin.dispatch; + Require(begin.dispatch == dispatch && begin.value == index && begin.event == 16); + const Event body = Take(Kind::Body); + Require(body.value == index && body.argument == reinterpret_cast(&context)); + const Event end = Take(Kind::Job); + Require(end.dispatch == dispatch && end.value == index && end.event == 32); + } + const Event result = Take(Kind::Dispatch); + Require(result.name == name && result.sequence == sequence && result.dispatch == dispatch && result.value == count); + Require(result.entry == reinterpret_cast(&Job) && result.argument == reinterpret_cast(&context)); + return dispatch; + } + + void Done() const { Require(next == records.size()); } +}; + +void EmptyAndIdentityCase() { + Fixture fixture; + const char* cpuName = "cpu"; + const char* gpuName = "compute"; + ISequence* cpu0 = fixture.Create(cpuName); + ISequence* cpu1 = fixture.Create(nullptr); + ISequence* gpu0 = fixture.Create(gpuName, true); + ISequence* gpu1 = fixture.Create(nullptr, true); + ISequence* unused = nullptr; + Require(fixture.factory->Create(&unused, 1, 1, 0, 1, "overflow") == static_cast(0x81580006u)); + Require(fixture.factory->Create(nullptr, 1, 1, 0, 1, "null") == static_cast(0x81580001u)); + for (ISequence* sequence : {cpu0, cpu1, gpu0, gpu1}) { + Require(sequence->SyncPoint() == 0); + Require(sequence->Wait() == 0); + } + Require(fixture.factory->Destroy(cpu0) == 0); + Require(fixture.factory->Destroy(cpu1) == 0); + Require(fixture.factory->DestroyCompute(gpu0) == 0); + Require(fixture.factory->DestroyCompute(gpu1) == 0); + ISequence* reused = fixture.Create(nullptr); + Require(fixture.factory->Destroy(reused) == 0); + fixture.Stop(); + Reader reader(fixture.id); + reader.Sequence(cpuName, 0, 1); + reader.Sequence(nullptr, 1, 1); + reader.Sequence(gpuName, 0, 1); + reader.Sequence(nullptr, 1, 1); + reader.Sequence(cpuName, 0, 2); + reader.Sequence(nullptr, 1, 2); + reader.Sequence(gpuName, 0, 2); + reader.Sequence(nullptr, 1, 2); + reader.Sequence(nullptr, 0, 1); + reader.Sequence(nullptr, 0, 2); + reader.Done(); +} + +void BatchCase(bool sync, bool wait, bool compute = false) { + Fixture fixture; + const char* name = "sequence"; + const char* firstName = "first"; + const char* secondName = "second"; + ISequence* sequence = fixture.Create(name, compute); + const std::uint16_t id = compute ? 2 : 0; + Context first; + Context second; + first.released.store(false); + Require(sequence->Dispatch(Job, &first, 1, firstName) == 0); + Await(first.entered); + if (sync) Require(sequence->SyncPoint() == 0); + if (wait) { + first.released.store(true); + Require(sequence->Wait() == 0); + } + Require(sequence->Dispatch(Job, &second, 4, secondName) == 0); + first.released.store(true); + Require(sequence->Wait() == 0); + if (compute) Require(fixture.factory->DestroyCompute(sequence) == 0); + else Require(fixture.factory->Destroy(sequence) == 0); + fixture.Stop(); + Reader reader(fixture.id); + reader.Sequence(name, 0, 1); + reader.Sequence(name, id, 3); + const auto firstId = reader.Dispatch(firstName, first, 1, id); + if (sync || wait) { + reader.Sequence(name, id, 4); + reader.Sequence(name, id, 3); + } + const auto secondId = reader.Dispatch(secondName, second, 4, id); + if (!wait) Require(firstId != secondId); + reader.Sequence(name, id, 4); + reader.Sequence(name, 0, 2); + reader.Done(); +} + +void ConcurrentCase() { + Fixture fixture(4); + ISequence* sequence = fixture.Create(nullptr); + Context context; + context.released.store(false); + Require(sequence->Dispatch(Job, &context, 8, nullptr) == 0); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (context.calls.load() < 4 && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + Require(context.calls.load() == 4); + context.released.store(true); + Require(sequence->Wait() == 0); + Require(fixture.factory->Destroy(sequence) == 0); + fixture.Stop(); + Reader reader(fixture.id); + reader.Sequence(nullptr, 0, 1); + reader.Sequence(nullptr, 0, 3); + std::array states{}; + std::uint16_t dispatch = 0; + for (std::size_t index = 0; index < states.size() * 3; ++index) { + Require(reader.next < reader.records.size()); + const Event event = reader.records[reader.next++]; + Require(event.value < states.size()); + auto& state = states[event.value]; + if (event.kind == Kind::Job) { + if (index == 0) dispatch = event.dispatch; + Require(event.manager == fixture.id && event.dispatch == dispatch); + Require((state == 0 && event.event == 16) || (state == 2 && event.event == 32)); + } else { + Require(event.kind == Kind::Body && state == 1 && event.argument == reinterpret_cast(&context)); + } + ++state; + } + for (const auto state : states) Require(state == 3); + const Event metadata = reader.Take(Kind::Dispatch); + Require(metadata.dispatch == dispatch && metadata.value == 8 && metadata.sequence == 0 && metadata.name == nullptr); + Require(metadata.entry == reinterpret_cast(&Job) && metadata.argument == reinterpret_cast(&context)); + reader.Sequence(nullptr, 0, 4); + reader.Sequence(nullptr, 0, 2); + reader.Done(); +} + +void GpuBarrierCase(bool mixed, bool cpuAfter) { + Fixture fixture; + ISequence* sequence = fixture.Create(nullptr, true); + alignas(256) static const std::array code{0xbf810000u}; + const auto address = reinterpret_cast(code.data()); + const std::array registers{{{0x20c, static_cast(address >> 8)}, {0x20d, static_cast(address >> 40) & 0xff}, {0x207, 1}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 0}}}; + const std::array specials{0, 0, 0, 0, 1, 0, 0, 0}; + const ShaderRegister* registerPointer = registers.data(); + const std::uint32_t* specialPointer = specials.data(); + std::array shader{}; + std::memcpy(shader.data() + 0x20, ®isterPointer, sizeof(registerPointer)); + std::memcpy(shader.data() + 0x28, &specialPointer, sizeof(specialPointer)); + shader[0x5c] = static_cast(registers.size()); + alignas(64) std::uint32_t gate = 0; + alignas(256) std::array commands{}; + CommandBuffer buffer{commands.data(), commands.data() + commands.size(), commands.data(), commands.data() + commands.size(), nullptr, nullptr, 0}; + Require(sceAgcAcbWaitRegMem(&buffer, 0, 3, 3, &gate, 1, 0xffffffffu, 0x190) != nullptr); + const Packet packet{commands.data(), static_cast(buffer.cursor_up - commands.data()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x21, &packet) == 0); + Context context; + if (mixed) { + context.released.store(false); + Require(sequence->Dispatch(Job, &context, 1, nullptr) == 0); + Await(context.entered); + } + Require(sequence->DispatchCompute(shader.data(), nullptr, 1, 1, 1, nullptr) == 0); + Require(sequence->SyncPoint() == 0); + if (cpuAfter) { + Require(sequence->Dispatch(Job, &context, 1, nullptr) == 0); + Require(sequence->SyncPoint() == 0); + } + Require(sequence->DispatchCompute(shader.data(), nullptr, 1, 1, 1, nullptr) == 0); + context.released.store(true); + std::atomic_ref(gate).store(1); + Require(sequence->Wait() == 0); + Require(fixture.factory->DestroyCompute(sequence) == 0); + fixture.Stop(); + Reader reader(fixture.id); + reader.Sequence(nullptr, 0, 1); + reader.Sequence(nullptr, 2, 3); + if (cpuAfter) { + reader.Sequence(nullptr, 2, 4); + reader.Sequence(nullptr, 2, 3); + } + if (mixed || cpuAfter) { + reader.Dispatch(nullptr, context, 1, 2); + reader.Sequence(nullptr, 2, 4); + reader.Sequence(nullptr, 2, 3); + } + reader.Sequence(nullptr, 2, 4); + reader.Sequence(nullptr, 0, 2); + reader.Done(); +} + +void ShutdownCase(bool queued) { + Fixture fixture; + ISequence* sequence = fixture.Create(nullptr); + Context first; + Context canceled; + first.released.store(false); + Require(sequence->Dispatch(Job, &first, 1, nullptr) == 0); + Await(first.entered); + if (queued) { + Require(sequence->SyncPoint() == 0); + Require(sequence->Dispatch(Job, &canceled, 1, "canceled") == 0); + } + std::atomic stopping{false}; + std::thread stopper([&] { + stopping.store(true); + fixture.Stop(); + }); + Await(stopping); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + first.released.store(true); + stopper.join(); + Require(!canceled.entered.load()); + Reader reader(fixture.id); + reader.Sequence(nullptr, 0, 1); + reader.Sequence(nullptr, 0, 3); + reader.Dispatch(nullptr, first, 1); + if (queued) { + reader.Sequence(nullptr, 0, 4); + reader.Sequence(nullptr, 0, 3); + } + reader.Done(); +} + +} + +extern "C" { + +int APS5_VABI sceRazorCpuJobManagerSequence(const char* name, std::uint16_t manager, std::uint16_t sequence, std::uint16_t event) { + Record({Kind::Sequence, name, manager, sequence, 0, 0, event, 0, 0}); + return -1; +} + +int APS5_VABI sceRazorCpuJobManagerJob(std::uint16_t manager, std::uint16_t dispatch, std::uint16_t index, std::uint16_t event) { + Record({Kind::Job, nullptr, manager, 0, dispatch, index, event, 0, 0}); + return -1; +} + +int APS5_VABI sceRazorCpuJobManagerDispatch(const char* name, std::uint16_t manager, std::uint16_t sequence, std::uint16_t dispatch, std::uint16_t count, std::uint64_t entry, std::uint64_t argument) { + Record({Kind::Dispatch, name, manager, sequence, dispatch, count, 0, entry, argument}); + return -1; +} + +} + +int main(int argc, char** argv) { + if (argc == 2 && std::strcmp(argv[1], "gpu") == 0) { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); + GpuBarrierCase(false, false); + GpuBarrierCase(false, true); + GpuBarrierCase(true, false); + AgcDriverShutdown_nid_postfix(); + std::puts("job manager GPU profiler tests passed"); + return 0; + } + if (argc != 1) return 2; + EmptyAndIdentityCase(); + BatchCase(false, false); + BatchCase(true, false); + BatchCase(false, true); + BatchCase(true, false, true); + ConcurrentCase(); + ShutdownCase(false); + ShutdownCase(true); + std::puts("job manager profiler tests passed"); +} diff --git a/core/libs/tests/JobManagerReference.cpp b/core/libs/tests/JobManagerReference.cpp index 690f118f53..f3934c5cf6 100644 --- a/core/libs/tests/JobManagerReference.cpp +++ b/core/libs/tests/JobManagerReference.cpp @@ -1,11 +1,15 @@ -#include "prx/libSceJobManager/include/Api.hpp" +#include "prx/libSceJobManager/libSceJobManager.hpp" #include "SceTypes.hpp" #include #include #include +#ifdef _WIN32 #include +#else +#include +#include +#endif -struct SequenceFactory; int RunJobManagerCase(const char* name); @@ -13,6 +17,7 @@ template static T Resolve(const char* nid) { const bool host = std::getenv("ANYPS5_JOB_MANAGER_HOST") != nullptr; const char* library = host ? "libSceJobManager.prx" : "libSceJobManager.prx.guest.prx"; +#ifdef _WIN32 HMODULE module = GetModuleHandleA(library); if (module == nullptr && host) module = LoadLibraryA("libs\\libSceJobManager.prx"); const FARPROC symbol = module != nullptr ? GetProcAddress(module, nid) : nullptr; @@ -20,6 +25,15 @@ static T Resolve(const char* nid) { std::fprintf(stderr, "JobManager reference: missing %s in %s (Windows error %lu)\n", nid, library, GetLastError()); std::exit(3); } +#else + static void* module = dlopen(host ? "libs/libSceJobManager.prx" : "app0/sce_module/libSceJobManager.prx.guest.prx", RTLD_NOW | RTLD_LOCAL); + const std::string name = host ? nid : std::string(nid) + "#guest"; + void* symbol = module != nullptr ? dlsym(module, name.c_str()) : nullptr; + if (symbol == nullptr) { + std::fprintf(stderr, "JobManager reference: missing %s in %s (%s)\n", name.c_str(), library, dlerror()); + std::exit(3); + } +#endif return reinterpret_cast(symbol); } @@ -55,12 +69,12 @@ int APS5_VABI _ZN3sce3Job10JobManager8shutdownEv(void* self) { return call(self); } -SequenceFactory* APS5_VABI _ZN3sce3Job10JobManager27getSequenceFactoryInterfaceENS1_8PriorityE(void* self, std::uint32_t priority) { +ISequenceFactory* APS5_VABI _ZN3sce3Job10JobManager27getSequenceFactoryInterfaceENS1_8PriorityE(void* self, std::uint32_t priority) { static const auto call = Resolve("W-yXgQmaozU"); return call(self, priority); } -SequenceFactory* APS5_VABI _ZN3sce3Job10JobManager34getUnifiedSequenceFactoryInterfaceENS1_8PriorityE(void* self, std::uint32_t priority) { +ISequenceFactory* APS5_VABI _ZN3sce3Job10JobManager34getUnifiedSequenceFactoryInterfaceENS1_8PriorityE(void* self, std::uint32_t priority) { static const auto call = Resolve("L5jTAtk3HKA"); return call(self, priority); } @@ -72,7 +86,12 @@ int APS5_VABI _ZN3sce3Job10JobManager24setPersistentThreadCountEj(void* self, st int APS5_VABI Run_nid_no_patch() { const char* name = std::getenv("ANYPS5_JOB_MANAGER_CASE"); - return name != nullptr ? RunJobManagerCase(name) : 2; + const int result = name != nullptr ? RunJobManagerCase(name) : 2; +#ifdef _WIN32 + return result; +#else + std::exit(result); +#endif } } diff --git a/core/libs/tests/JobManagerRegistration.cpp b/core/libs/tests/JobManagerRegistration.cpp new file mode 100644 index 0000000000..056b51a6b3 --- /dev/null +++ b/core/libs/tests/JobManagerRegistration.cpp @@ -0,0 +1,86 @@ +#include "prx/libSceJobManager/libSceJobManager.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + +namespace { + +alignas(256) std::array workArea{}; +alignas(64) std::array manager{}; +std::uint32_t registrationCalls = 0; +std::uint32_t unregistrationCalls = 0; +std::uint32_t assignedId = 0; +bool writeId = true; +int registrationResult = 0; +std::atomic jobs{0}; + +void Require(bool condition) { + if (!condition) std::abort(); +} + +int Initialize(bool unified, void* memory) { + if (unified) return _ZN3sce3Job10JobManager10initializeEPvmjjjmPKNS1_12ConfigParamsE(manager.data(), memory, workArea.size(), 1, 1, 0, 0, nullptr); + return _ZN3sce3Job10JobManager10initializeEPvmjjPKNS1_12ConfigParamsE(manager.data(), memory, workArea.size(), 1, 1, nullptr); +} + +void APS5_VABI Count(void*, std::uint32_t) { + ++jobs; +} + +} + +extern "C" { + +int APS5_VABI _sceUlobjmgrRegisterObject_nid_postfix(std::uint64_t object, std::int32_t kind, std::uint32_t* id) { + Require(object == reinterpret_cast(workArea.data()) && kind == 0x21 && id != nullptr); + Require(workArea[0] == 0x4a && workArea[1] == 0x4d); + Require(_ZN3sce3Job10JobManager27getSequenceFactoryInterfaceENS1_8PriorityE(manager.data(), 0) != nullptr); + ++registrationCalls; + if (writeId) *id = assignedId; + return registrationResult; +} + +int APS5_VABI _sceUlobjmgrUnregisterObject_nid_postfix(std::uint32_t id) { + Require(id == (writeId ? assignedId : 0)); + Require(_ZN3sce3Job10JobManager27getSequenceFactoryInterfaceENS1_8PriorityE(manager.data(), 0) != nullptr); + ++unregistrationCalls; + return 37; +} + +} + +int main() { + for (const bool unified : {false, true}) { + for (const auto [result, assignsId] : std::array, 5>{{{0, true}, {22, true}, {static_cast(0x8002000cu), true}, {22, false}, {static_cast(0x8002000cu), false}}}) { + for (const bool destructor : {false, true}) { + _ZN3sce3Job10JobManagerC1Ev(manager.data()); + const auto before = registrationCalls; + Require(Initialize(unified, nullptr) == static_cast(0x81581001u)); + Require(registrationCalls == before); + registrationResult = result; + writeId = assignsId; + assignedId = 101 + before; + Require(Initialize(unified, workArea.data()) == result); + Require(registrationCalls == before + 1 && unregistrationCalls == before); + Require(Initialize(unified, workArea.data()) == static_cast(0x81581020u)); + Require(registrationCalls == before + 1); + auto* factory = _ZN3sce3Job10JobManager27getSequenceFactoryInterfaceENS1_8PriorityE(manager.data(), 0); + ISequence* sequence = nullptr; + Require(factory->Create(&sequence, 3, 1, 0, 1, nullptr) == 0); + jobs = 0; + Require(sequence->Dispatch(Count, nullptr, 3, nullptr) == 0); + Require(factory->Destroy(sequence) == 0 && jobs == 3); + if (destructor) _ZN3sce3Job10JobManagerD1Ev(manager.data()); + else Require(_ZN3sce3Job10JobManager8shutdownEv(manager.data()) == 0); + Require(unregistrationCalls == before + 1); + Require(_ZN3sce3Job10JobManager8shutdownEv(manager.data()) == static_cast(0x81581010u)); + _ZN3sce3Job10JobManagerD1Ev(manager.data()); + Require(unregistrationCalls == before + 1); + } + } + } + std::puts("job manager registration tests passed"); +} diff --git a/core/libs/tests/JsonCopyFailure.cpp b/core/libs/tests/JsonCopyFailure.cpp index 9ca2af1239..b8e2562656 100644 --- a/core/libs/tests/JsonCopyFailure.cpp +++ b/core/libs/tests/JsonCopyFailure.cpp @@ -55,6 +55,107 @@ static void CheckValue(const Value& value, const char* expected) { } +struct HandlerContext { + Value fallback{}; + const Value* parent = nullptr; + unsigned calls = 0; +}; + +static const Value& APS5_VABI OnTypeMismatch(ValueType requested, const Value* value, const Value* second, void* context) { + auto& handler = *static_cast(context); + Require(requested == 2 && value == handler.parent && second == nullptr, "handler arguments changed"); + ++handler.calls; + return handler.fallback; +} + +static void CheckHandler(HandlerContext& handler) { + Require(*_ZNK3sce4Json5Value10getIntegerEv(handler.parent) == 77 && handler.calls == 1, "handler was lost"); +} + +static int APS5_VABI ReadNext(char& character, void* context) { + auto& cursor = *static_cast(context); + character = *cursor; + if (!character) return -1; + ++cursor; + return 0; +} + +static bool Parse(const char* document, bool valid, bool callback, const char* expected = nullptr) { + Value result{}; + HandlerContext handler{}; + _ZN3sce4Json5ValueC1EPKc(&result, "previous"); + _ZN3sce4Json5ValueC1El(&handler.fallback, 77); + handler.parent = &result; + Require(_ZN3sce4Json5Value22setTypeMismatchHandlerEPFRKS1_NS0_9ValueTypeEPS2_S5_PvES6_(&result, OnTypeMismatch, &handler) == 0, "set handler failed"); + bool completed = false; + allocationBudget = activeBudget; + try { + const char* cursor = document; + const int status = callback ? _ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(&result, ReadNext, &cursor) + : _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&result, document, std::strlen(document)); + Require(status == (valid ? 0 : (callback ? -1 : static_cast(0x80848101))), "unexpected parse result"); + completed = true; + } catch (const std::bad_alloc&) { + } + allocationBudget = -1; + CheckValue(result, completed && valid ? (expected ? expected : document) : "\"previous\""); + CheckHandler(handler); + _ZN3sce4Json5ValueD1Ev(&result); + _ZN3sce4Json5ValueD1Ev(&handler.fallback); + return completed; +} + +static bool ObjectEntry(const char* keyText) { + Object object{}; + String oldKey{}, newKey{}; + HandlerContext handler{}; + _ZN3sce4Json6ObjectC1Ev(&object); + _ZN3sce4Json6StringC1EPKc(&oldKey, "kept"); + _ZN3sce4Json6StringC1EPKc(&newKey, keyText); + Value* kept = _ZN3sce4Json6ObjectixERKNS0_6StringE(&object, &oldKey); + Require(_ZN3sce4Json5Value3setEPKc(kept, "previous") == 0, "prepare entry failed"); + _ZN3sce4Json5ValueC1El(&handler.fallback, 77); + handler.parent = kept; + Require(_ZN3sce4Json5Value22setTypeMismatchHandlerEPFRKS1_NS0_9ValueTypeEPS2_S5_PvES6_(kept, OnTypeMismatch, &handler) == 0, "set handler failed"); + bool completed = false; + Value* inserted = nullptr; + allocationBudget = activeBudget; + try { + inserted = _ZN3sce4Json6ObjectixERKNS0_6StringE(&object, &newKey); + completed = true; + } catch (const std::bad_alloc&) { + } + allocationBudget = 0; + Require(_ZN3sce4Json6ObjectixERKNS0_6StringE(&object, &oldKey) == kept, "existing entry changed"); + allocationBudget = -1; + Require(_ZNK3sce4Json6Object4sizeEv(&object) == (completed ? 2 : 1), "failed insertion changed object size"); + if (completed) Require(inserted && _ZNK3sce4Json5Value7getTypeEv(inserted) == 0, "new entry is not null"); + CheckValue(*kept, "\"previous\""); + CheckHandler(handler); + _ZN3sce4Json6ObjectD1Ev(&object); + _ZN3sce4Json6StringD1Ev(&newKey); + _ZN3sce4Json6StringD1Ev(&oldKey); + _ZN3sce4Json5ValueD1Ev(&handler.fallback); + return completed; +} + +template +static unsigned CheckFailures(const char* name, TOperation operation) { + activeCase = name; + bool succeeded = false; + unsigned failures = 0; + for (activeBudget = 0; activeBudget < 256 && !succeeded; ++activeBudget) { + const auto before = liveAllocations; + succeeded = operation(); + if (liveAllocations != before) std::fprintf(stderr, "expected %zu live allocations, got %zu\n", before, liveAllocations); + Require(liveAllocations == before, "operation leaked allocations"); + failures += !succeeded; + } + Require(succeeded, "operation never succeeded"); + std::printf("%s: %u allocation failures, no leaks\n", name, failures); + return failures; +} + enum class Operation { ValueCopy, ArrayCopy, ObjectCopy, ValueAssign, ObjectAssign, ArrayAppend, ValueArray, ValueObject, ValueString, ValueText, ValueTypeString, ValueTypeArray, @@ -169,18 +270,12 @@ int main() { }; unsigned failures = 0; for (const auto& test : cases) { - activeCase = test.name; - bool succeeded = false; - for (activeBudget = 0; activeBudget < 256 && !succeeded; ++activeBudget) { - const auto before = liveAllocations; - succeeded = Run(test.operation, *test.source, test.expected, *array, *object, text); - if (liveAllocations != before) std::fprintf(stderr, "expected %zu live allocations, got %zu\n", before, liveAllocations); - Require(liveAllocations == before, "copy leaked allocations"); - failures += !succeeded; + failures += CheckFailures(test.name, [&] { + const bool succeeded = Run(test.operation, *test.source, test.expected, *array, *object, text); CheckValue(objectValue, objectJson); CheckValue(arrayValue, arrayJson); - } - Require(succeeded, "copy never succeeded"); + return succeeded; + }); } const auto checkIterator = [&]() { constexpr bool objectIterator = TObject; @@ -242,10 +337,30 @@ int main() { CheckValue(arrayValue, arrayJson); } Require(swapSucceeded, "swap never succeeded"); + struct ParseCase { const char* name; const char* document; bool valid; bool callback; const char* expected = nullptr; }; + const ParseCase parseCases[] = { + {"parse empty array", "[]", true, false}, + {"parse empty object", "{}", true, false}, + {"parse array", "[1]", true, false}, + {"parse object", "{\"a\":1}", true, false}, + {"parse nested array", arrayJson, true, false}, + {"parse nested object", objectJson, true, false}, + {"parse long key", "{\"a sufficiently long key to require its own allocation\":{\"child\":[1,2]}}", true, false}, + {"parse invalid array", "[1,{\"a\":[true,null]}", false, false}, + {"parse invalid object", "{\"a\":[{\"b\":\"a sufficiently long string allocation\"},[true,null,17]]", false, false}, + {"parse trailing input", "{\"a\":[1]}x", true, false, "{\"a\":[1]}"}, + {"parse callback", objectJson, true, true}, + {"parse invalid callback", "[{\"a\":[1]}", false, true}, + }; + for (const auto& test : parseCases) + failures += CheckFailures(test.name, [&] { return Parse(test.document, test.valid, test.callback, test.expected); }); + failures += CheckFailures("insert short key", [] { return ObjectEntry("missing"); }); + failures += CheckFailures("insert long key", [&] { return ObjectEntry(_ZNK3sce4Json6String5c_strEv(&text)); }); _ZN3sce4Json6StringD1Ev(&text); _ZN3sce4Json5ValueD1Ev(&arrayValue); _ZN3sce4Json5ValueD1Ev(&objectValue); Require(liveAllocations == initial, "source cleanup leaked allocations"); - std::printf("JSON copy cleanup: %zu operations, %u allocation failures, no leaks\n", sizeof(cases) / sizeof(cases[0]) + 3, failures); + std::printf("JSON allocation cleanup: %zu operations, %u allocation failures, no leaks\n", + sizeof(cases) / sizeof(cases[0]) + sizeof(parseCases) / sizeof(parseCases[0]) + 5, failures); } diff --git a/core/libs/tests/JxrStream.cpp b/core/libs/tests/JxrStream.cpp new file mode 100644 index 0000000000..b847bbb8d8 --- /dev/null +++ b/core/libs/tests/JxrStream.cpp @@ -0,0 +1,236 @@ +#include "prx/libSceJxr/libSceJxr.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libkernel/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +namespace { + +constexpr std::size_t PacketSize = 4096; +alignas(8192) std::array ringStorage; +std::span ring(reinterpret_cast((reinterpret_cast(ringStorage.data()) + 16383) & ~std::uintptr_t{16383}), 8192); +std::array output; +std::vector calls; +void* lastBuffer; +std::size_t lastSize; +std::size_t lastPosition; +std::int64_t readResult; +std::int64_t writeResult; +std::int64_t seekResult; +Jxr::Stream* lastStream; + +struct Mutation { + Jxr::BitIo* io = nullptr; + std::uint8_t* afterSeek = nullptr; + Jxr::Stream* afterSeekStream = nullptr; + std::uint8_t* afterRead = nullptr; + std::uint8_t* afterWrite = nullptr; +} mutation; + +void Require(bool result, const char* message) { + if (result) return; + std::fprintf(stderr, "JXR stream: %s\n", message); + std::abort(); +} + +std::int64_t APS5_VABI Read(Jxr::Stream* stream, void* buffer, std::size_t size) { + calls.push_back('r'); + lastStream = stream; + lastBuffer = buffer; + lastSize = size; + auto* bytes = static_cast(buffer); + for (std::size_t i = 0; i < size; ++i) bytes[i] = static_cast(i * 17 + lastPosition); + if (mutation.afterRead) mutation.io->start = mutation.afterRead; + return readResult; +} + +std::int64_t APS5_VABI Write(Jxr::Stream*, const void* buffer, std::size_t size) { + calls.push_back('w'); + lastBuffer = const_cast(buffer); + lastSize = size; + Require(size == output.size(), "write length differs from one packet"); + std::memcpy(output.data(), buffer, size); + if (mutation.afterWrite) mutation.io->start = mutation.afterWrite; + return writeResult; +} + +std::int64_t APS5_VABI SetPosition(Jxr::Stream*, std::size_t position) { + calls.push_back('s'); + lastPosition = position; + if (mutation.afterSeek) mutation.io->start = mutation.afterSeek; + if (mutation.afterSeekStream) mutation.io->stream = mutation.afterSeekStream; + return seekResult; +} + +void Reads(Jxr::Stream& stream) { + for (std::size_t half : {std::size_t{0}, PacketSize}) { + for (std::size_t position : {std::size_t{0}, std::size_t{91}, std::numeric_limits::max() - 100}) { + for (std::int64_t result : {std::int64_t{0}, std::int64_t{-73}, std::int64_t{0x100000001}}) { + std::fill(ring.begin(), ring.end(), 0xcc); + calls.clear(); + readResult = result; + seekResult = -99; + Jxr::BitIo io{0x77777777, 0x12345678, 11, ~8192, ring.data() + half, ring.data() + (half ^ PacketSize), &stream, position}; + const auto initial = io; + Require(readIS_nid_postfix(nullptr, &io) == 0, "read helper propagated an ignored callback error"); + Require(calls == std::vector({'s', 'r'}), "read callbacks are absent or out of order"); + Require(lastPosition == position && lastSize == PacketSize && lastBuffer == initial.start, "read callback arguments differ"); + Require(io.referenceOffset == position + PacketSize && io.start == ring.data() + (half ^ PacketSize), "read cursor did not advance and wrap"); + Require(io.current == initial.current && io.accumulator == initial.accumulator && io.bitsUsed == initial.bitsUsed && io.mask == initial.mask && io.stream == initial.stream, "read changed unrelated bit state"); + std::uint32_t shadow; + std::memcpy(&shadow, initial.start, sizeof(shadow)); + Require(io.shadow == shadow, "read shadow is not the first four bytes in host order"); + for (std::size_t i = 0; i < PacketSize; ++i) { + Require(initial.start[i] == static_cast(i * 17 + position), "refill data differs"); + Require(ring[(half ^ PacketSize) + i] == 0xcc, "refill changed the other half of the ring"); + } + } + } + } +} + +void Writes(Jxr::Stream& stream) { + for (std::size_t i = 0; i < ring.size(); ++i) ring[i] = static_cast(i * 23); + for (std::size_t half : {std::size_t{0}, PacketSize}) { + for (std::int64_t result : {std::int64_t{0}, std::int64_t{-73}, std::int64_t{0x100000001}, std::numeric_limits::min()}) { + calls.clear(); + writeResult = result; + Jxr::BitIo io{7, 19, 13, ~8192, ring.data() + half, ring.data() + (half ^ PacketSize), &stream, 1234}; + const auto initial = io; + Require(writeIS_nid_postfix(nullptr, &io) == result, "write result was truncated or changed"); + Require(calls == std::vector({'w'}) && lastBuffer == initial.start && lastSize == PacketSize, "write callback arguments differ"); + Require(std::equal(output.begin(), output.end(), initial.start), "write data differs from buffered bytes"); + Require(io.start == (result < 0 ? initial.start : ring.data() + (half ^ PacketSize)), "failed write advanced or successful write did not wrap"); + Require(io.shadow == initial.shadow && io.accumulator == initial.accumulator && io.bitsUsed == initial.bitsUsed && io.mask == initial.mask && io.current == initial.current && io.stream == initial.stream && io.referenceOffset == initial.referenceOffset, "write changed unrelated bit state"); + } + } +} + +void Buffered() { + for (std::size_t half : {std::size_t{0}, PacketSize}) { + for (std::size_t consumed : {std::size_t{0}, PacketSize - 1}) { + Jxr::BitIo io{123, 456, 7, ~8192, ring.data() + half, ring.data() + half + consumed, nullptr, 789}; + const auto initial = io; + calls.clear(); + Require(readIS_nid_postfix(nullptr, &io) == 0 && writeIS_nid_postfix(nullptr, &io) == 0, "buffered data unexpectedly required a stream"); + Require(calls.empty() && std::memcmp(&initial, &io, sizeof(io)) == 0, "buffered operation mutated state or performed I/O"); + } + } +} + +void CallbackMutation(Jxr::Stream& stream) { + Jxr::Stream replacement{}; + readResult = -73; + seekResult = -99; + std::fill(ring.begin(), ring.end(), 0xcc); + calls.clear(); + Jxr::BitIo io{7, 19, 13, ~8192, ring.data(), ring.data() + PacketSize, &stream, 91}; + mutation = {&io, ring.data() + PacketSize, &replacement, nullptr, nullptr}; + Require(readIS_nid_postfix(nullptr, &io) == 0, "seek mutation read failed"); + Require(calls == std::vector({'s', 'r'}) && lastStream == &stream, "read did not retain the stream selected before seek"); + Require(lastBuffer == ring.data() + PacketSize && lastSize == PacketSize, "read did not use the buffer selected by seek"); + Require(io.stream == &replacement && io.start == ring.data() && io.referenceOffset == 91 + PacketSize, "seek mutation was lost before wrapping"); + std::uint32_t shadow; + std::memcpy(&shadow, ring.data() + PacketSize, sizeof(shadow)); + Require(io.shadow == shadow && ring.front() == 0xcc, "seek-selected buffer shadow differs"); + + std::fill(ring.begin(), ring.end(), 0xab); + calls.clear(); + io = {7, 19, 13, ~8192, ring.data(), ring.data() + PacketSize, &stream, 123}; + mutation = {&io, nullptr, nullptr, ring.data() + PacketSize, nullptr}; + Require(readIS_nid_postfix(nullptr, &io) == 0, "read mutation failed"); + Require(lastBuffer == ring.data() && io.start == ring.data(), "read mutation did not precede final wrap"); + Require(io.shadow == 0xabababab && io.referenceOffset == 123 + PacketSize, "shadow did not use the pointer changed by read"); + + for (std::int64_t result : {std::int64_t{0}, std::int64_t{0x100000001}, std::int64_t{-73}}) { + std::fill(ring.begin(), ring.end(), 0xef); + calls.clear(); + io = {7, 19, 13, ~8192, ring.data(), ring.data() + PacketSize, &stream, 789}; + mutation = {&io, nullptr, nullptr, nullptr, ring.data() + PacketSize}; + writeResult = result; + Require(writeIS_nid_postfix(nullptr, &io) == result, "mutating write result differs"); + Require(calls == std::vector({'w'}) && lastBuffer == ring.data(), "mutating write buffer differs"); + Require(io.start == ring.data() + (result < 0 ? PacketSize : 0), "write callback mutation did not precede success wrap or failed return"); + Require(io.referenceOffset == 789 && io.shadow == 7, "mutating write changed unrelated state"); + } + mutation = {}; +} + +#ifdef _WIN32 +std::int64_t APS5_VABI ObserveRead(Jxr::Stream*, void*, std::size_t) { + calls.push_back('r'); + return 0; +} +#endif + +void GuestMapping(Jxr::Stream& stream) { + constexpr std::size_t mappingSize = 0x4000; + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, mappingSize, 0, 0, &physical) == 0, "allocate guest packet backing"); + void* mapping = nullptr; + Require(sceKernelMapDirectMemory(&mapping, mappingSize, 3, 0, physical, 0) == 0 && mapping, "map guest packet backing"); + Require(GuestArena::GuestArenaContains_nid_postfix(mapping, mappingSize), "packet mapping is outside the guest arena"); + auto* bytes = static_cast(mapping); + std::fill_n(bytes, mappingSize, 0xcc); + calls.clear(); + readResult = 0; + seekResult = 0; + Jxr::BitIo io{7, 19, 13, ~8192, bytes, bytes + PacketSize, &stream, 91}; + Require(readIS_nid_postfix(nullptr, &io) == 0, "guest packet refill failed"); + Require(calls == std::vector({'s', 'r'}) && lastBuffer == bytes && lastSize == PacketSize, "guest packet callback arguments differ"); + Require(io.start == bytes + PacketSize && io.referenceOffset == 91 + PacketSize, "guest packet refill did not advance"); + for (std::size_t i = 0; i < mappingSize; ++i) { + Require(bytes[i] == (i < PacketSize ? static_cast(i * 17 + 91) : 0xcc), "guest packet refill changed unexpected bytes"); + } +#ifdef _WIN32 + Require(sceKernelMprotect(mapping, mappingSize, 1) == 0, "protect guest packet backing read-only"); + { + GuestArena::HostWrite access(bytes, PacketSize); + Require(!access.Open(), "read-only guest packet backing accepted a host write"); + } + Jxr::Stream observedStream = stream; + observedStream.read = ObserveRead; + io = {7, 19, 13, ~8192, bytes, bytes + PacketSize, &observedStream, 91}; + const auto initial = io; + std::array before; + std::copy_n(bytes, mappingSize, before.begin()); + calls.clear(); + bool rejected = false; + try { + readIS_nid_postfix(nullptr, &io); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected && calls == std::vector({'s'}), "rejected guest packet write invoked the read callback"); + Require(std::memcmp(&io, &initial, sizeof(io)) == 0 && std::equal(before.begin(), before.end(), bytes), "rejected guest packet write mutated state or backing"); + Require(sceKernelMprotect(mapping, mappingSize, 3) == 0, "restore writable guest packet backing"); +#endif + Require(sceKernelMunmap(reinterpret_cast(mapping), mappingSize) == 0, "unmap guest packet backing"); + Require(sceKernelReleaseDirectMemory(physical, mappingSize) == 0, "release guest packet backing"); +} + +} + +int main() { + Jxr::Stream stream{}; + stream.read = Read; + stream.write = Write; + stream.setPosition = SetPosition; + Reads(stream); + Writes(stream); + Buffered(); + CallbackMutation(stream); + GuestMapping(stream); + return 0; +} diff --git a/core/libs/tests/KernelError.cpp b/core/libs/tests/KernelError.cpp index 52c41c28db..d0c1aa0bac 100644 --- a/core/libs/tests/KernelError.cpp +++ b/core/libs/tests/KernelError.cpp @@ -12,14 +12,17 @@ static void Require(bool value) { if (!value) throw std::runtime_error("Kernel e int main() { *__error_nid_postfix() = 123; - const std::array, 6> cases{{ + const std::array, 8> cases{{ {0, 0}, {1, SCE_KERNEL_ERROR_EPERM}, {2, SCE_KERNEL_ERROR_ENOENT}, - {4, SCE_KERNEL_ERROR_EINTR}, {14, SCE_KERNEL_ERROR_EFAULT}, {22, SCE_KERNEL_ERROR_EINVAL} + {4, SCE_KERNEL_ERROR_EINTR}, {14, SCE_KERNEL_ERROR_EFAULT}, {22, SCE_KERNEL_ERROR_EINVAL}, + {95, SCE_KERNEL_ERROR_ENOTRECOVERABLE}, {96, SCE_KERNEL_ERROR_EOWNERDEAD} }}; for (const auto [input, expected] : cases) { Require(sceKernelError(input) == expected); Require(*__error_nid_postfix() == 123); } + Require(SCE_KERNEL_ERROR_ENOTRECOVERABLE == static_cast(0x8002005fu)); + Require(SCE_KERNEL_ERROR_EOWNERDEAD == static_cast(0x80020060u)); Require(sceKernelError(-1) == static_cast(0x8001ffffu)); Require(sceKernelError(std::numeric_limits::max()) == 0x1ffff); Require(sceKernelError(std::numeric_limits::min()) == 0x20000); diff --git a/core/libs/tests/NetBlockingSend.cpp b/core/libs/tests/NetBlockingSend.cpp index 466cbc279a..6dba4d4900 100644 --- a/core/libs/tests/NetBlockingSend.cpp +++ b/core/libs/tests/NetBlockingSend.cpp @@ -130,6 +130,14 @@ int main(int argc, char** argv) { { Connection connection; auto sending = std::async(std::launch::async, [&] { return Send(api, connection.sender, bytes); }); + const auto entered = std::chrono::steady_clock::now() + std::chrono::seconds(5); + char peeked = 0; + bool arrived = false; + while (!arrived && std::chrono::steady_clock::now() < entered) { + arrived = sceNetRecv(connection.receiver, &peeked, 1, NET_MSG_PEEK | NET_MSG_DONTWAIT) > 0; + if (!arrived) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + Require(arrived, "send did not reach the receiver before abort"); Require(sending.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout, "send did not wait before abort"); Require(sceNetSocketAbort(connection.sender, 0) == 0, "abort a blocked send"); Require(sending.wait_for(std::chrono::seconds(2)) == std::future_status::ready, "abort did not wake a blocked send"); diff --git a/core/libs/tests/SslContextLifetime.cpp b/core/libs/tests/SslContextLifetime.cpp new file mode 100644 index 0000000000..198e1c7cbe --- /dev/null +++ b/core/libs/tests/SslContextLifetime.cpp @@ -0,0 +1,182 @@ +#include "prx/libSceSsl/Export.cpp" + +#include +#include +#include +#include +#include +#include + +extern "C" bool KernelLookupSocketNative_nid_no_patch(int, std::shared_ptr*, std::uintptr_t*) { + std::fputs("unexpected native socket lookup in SSL lifetime test\n", stderr); + std::abort(); +} + +namespace { + +void Check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +int CreateContext() { + const int id = sceSslInit_nid_postfix(1024 * 1024); + Check(id > 0, "context creation failed"); + return id; +} + +int CreateConnection(int context) { + const int id = sceSslCreateConnection(context, -1, "localhost"); + Check(id > 0, "connection creation failed"); + return id; +} + +void DeleteBeforeTerm() { + for (int cycle = 0; cycle < 3; ++cycle) { + const int context = CreateContext(); + const int connection = CreateConnection(context); + Check(sceSslDeleteConnection(connection) == 0, "delete before term failed"); + Check(sceSslTerm_nid_postfix(context) == 0, "term after delete failed"); + Check(State().connections.empty() && State().contexts.empty(), "delete-first cycle retained records"); + } + std::puts("SSL delete-first control: 3 cycles, no retained records"); +} + +void TermBeforeDelete() { + bool clean = true; + for (int cycle = 0; cycle < 3; ++cycle) { + const auto before = State().connections.size(); + const int context = CreateContext(); + const int connection = CreateConnection(context); + const auto created = State().connections.size(); + Check(sceSslTerm_nid_postfix(context) == 0, "term before delete failed"); + const auto terminated = State().connections.size(); + Check(sceSslDeleteConnection(connection) == ErrorInvalidId, "post-term delete result changed"); + const auto deleted = State().connections.size(); + Check(State().contexts.empty(), "context survived termination"); + clean = clean && before == 0 && created == 1 && terminated == 0 && deleted == 0; + std::printf("SSL term-first cycle %d: connections %zu -> %zu -> %zu -> %zu\n", cycle + 1, before, created, terminated, deleted); + } + Check(clean, "termination retained orphan connection records"); +} + +void SeparateContexts() { + const int first = CreateContext(); + const int second = CreateContext(); + const int firstConnection = CreateConnection(first); + const int secondConnection = CreateConnection(second); + const int extraConnection = CreateConnection(first); + std::weak_ptr lifetime = State().contexts.at(first); + Check(sceSslTerm_nid_postfix(first) == 0, "selective term failed"); + Check(lifetime.expired(), "idle terminated context survived"); + Check(State().connections.size() == 1 && State().connections.contains(secondConnection), "term did not isolate connection ownership"); + Check(!State().connections.contains(firstConnection) && !State().connections.contains(extraConnection), "term retained a child connection"); + Check(sceSslTerm_nid_postfix(first) == ErrorInvalidId, "repeated term result changed"); + Check(sceSslCreateConnection(first, -1, "localhost") == ErrorInvalidId, "create accepted a terminated context"); + Check(sceSslDeleteConnection(firstConnection) == ErrorInvalidId, "delete accepted a terminated connection"); + Check(sceSslSetVerifyCallback(firstConnection, nullptr, nullptr) == ErrorInvalidId, "callback accepted a terminated connection"); + Check(sceSslConnect(firstConnection) == ErrorInvalidId && sceSslClose(firstConnection) == ErrorInvalidId, "I/O accepted a terminated connection"); + Check(sceSslSetVerifyCallback(secondConnection, nullptr, nullptr) == 0, "term invalidated another context's connection"); + Check(sceSslDeleteConnection(secondConnection) == 0 && sceSslTerm_nid_postfix(second) == 0, "unrelated context cleanup failed"); + Check(State().connections.empty() && State().contexts.empty(), "selective term retained records"); +} + +void ConcurrentLifecycle() { + for (int cycle = 0; cycle < 32; ++cycle) { + const int context = CreateContext(); + std::array connections{}; + for (auto& id : connections) id = CreateConnection(context); + std::barrier start(3); + std::atomic valid{true}; + std::thread creator([&] { + start.arrive_and_wait(); + for (int attempt = 0; attempt < 16; ++attempt) { + const int id = sceSslCreateConnection(context, -1, "localhost"); + if (id == ErrorInvalidId) continue; + if (id <= 0) { valid = false; continue; } + const int result = sceSslDeleteConnection(id); + if (result != 0 && result != ErrorInvalidId) valid = false; + } + }); + std::thread deleter([&] { + start.arrive_and_wait(); + for (const auto id : connections) { + const int result = sceSslDeleteConnection(id); + if (result != 0 && result != ErrorInvalidId) valid = false; + } + }); + start.arrive_and_wait(); + const int result = sceSslTerm_nid_postfix(context); + creator.join(); + deleter.join(); + Check(result == 0 && valid, "concurrent lifecycle returned an unexpected error"); + Check(State().connections.empty() && State().contexts.empty(), "concurrent termination retained records"); + } + std::puts("SSL concurrent create/delete/term: 32 cycles, no retained records"); +} + +struct CallbackState { + std::latch entered{1}; + std::latch resume{1}; + int context = 0; + bool valid = false; +}; + +int APS5_VABI PendingCallback(int context, unsigned int, void**, int count, void* argument) { + auto& state = *static_cast(argument); + state.valid = context == state.context && count == 0; + state.entered.count_down(); + state.resume.wait(); + return 0; +} + +void InFlightSession() { + const int context = CreateContext(); + const int connection = CreateConnection(context); + auto owner = State().contexts.at(context); + std::weak_ptr contextLifetime = owner; + auto session = std::make_shared(context, owner, std::shared_ptr{}, 0); + { + PoolScope scope(owner->pool); + session->Configure("localhost", "", InitialVerifyOptions); + } + Check(owner->pool.current > 0, "session did not allocate backend state"); + session->verified = true; + session->verifyResult = ErrorCallbackCheck; + State().connections.at(connection).session = session; + std::weak_ptr sessionLifetime = session; + CallbackState callback; + callback.context = context; + Check(sceSslSetVerifyCallback(connection, PendingCallback, &callback) == 0, "callback setup failed"); + owner.reset(); + session.reset(); + int operationResult = ErrorInvalidId; + std::thread operation([&] { operationResult = sceSslConnect(connection); }); + callback.entered.wait(); + const int result = sceSslTerm_nid_postfix(context); + const bool removed = State().connections.empty() && State().contexts.empty(); + const bool retained = !sessionLifetime.expired() && !contextLifetime.expired(); + callback.resume.count_down(); + operation.join(); + Check(result == 0 && removed, "term did not remove the in-flight connection"); + Check(retained, "term destroyed an in-flight session or its pool"); + Check(callback.valid && operationResult == 0, "in-flight verification callback did not finish"); + Check(sessionLifetime.expired() && contextLifetime.expired(), "finished operation retained its session or pool"); + Check(sceSslConnect(connection) == ErrorInvalidId && sceSslDeleteConnection(connection) == ErrorInvalidId, "post-term connection became usable"); + std::puts("SSL in-flight callback completed; session and pool released after completion"); +} + +} + +int main() { + try { + DeleteBeforeTerm(); + TermBeforeDelete(); + SeparateContexts(); + ConcurrentLifecycle(); + InFlightSession(); + std::puts("SSL context lifetime passed"); + } catch (const std::exception& error) { + std::fprintf(stderr, "SSL context lifetime failed: %s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/UltFinalizeAllocation.cpp b/core/libs/tests/UltFinalizeAllocation.cpp new file mode 100644 index 0000000000..c5730d7f4a --- /dev/null +++ b/core/libs/tests/UltFinalizeAllocation.cpp @@ -0,0 +1,72 @@ +#include "prx/libSceUlt/libSceUlt.hpp" +#include +#include +#include +#include +#include + +static int allocationBudget = -1; +static unsigned allocationAttempts = 0; + +void* operator new(std::size_t size) { + if (allocationBudget >= 0) { + ++allocationAttempts; + if (allocationBudget == 0) { + allocationBudget = -1; + throw std::bad_alloc(); + } + --allocationBudget; + } + if (void* memory = std::malloc(size ? size : 1)) return memory; + throw std::bad_alloc(); +} + +void operator delete(void* memory) noexcept { std::free(memory); } +void operator delete(void* memory, std::size_t) noexcept { ::operator delete(memory); } + +struct alignas(8) Object { std::uint8_t bytes[256]{}; }; + +static void Require(bool passed, const char* message) { + if (!passed) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +int main() { + Require(sceUltInitialize() == ULT_OK, "initialization failed"); + unsigned failures = 0; + unsigned rejectedCases = 0; + for (int budget = 0; budget < 8; ++budget) { + Object semaphore{}; + Object mutex{}; + Require(sceUltSemaphoreCreate(&semaphore, "semaphore", 1, nullptr, nullptr, 0) == ULT_OK, "semaphore creation failed"); + Require(sceUltMutexCreate(&mutex, "mutex", nullptr, nullptr, 0) == ULT_OK, "mutex creation failed"); + allocationAttempts = 0; + allocationBudget = budget; + bool rejected = false; + int outcome = ULT_OK; + try { outcome = sceUltFinalize(); } + catch (const std::bad_alloc&) { rejected = true; } + allocationBudget = -1; + + rejectedCases += rejected; + const int semaphoreResult = sceUltSemaphoreTryAcquire(&semaphore, 1); + const int mutexResult = sceUltMutexTryLock(&mutex); + if (semaphoreResult == ULT_OK) Require(sceUltSemaphoreRelease(&semaphore, 1) == ULT_OK, "semaphore release failed"); + if (mutexResult == ULT_OK) Require(sceUltMutexUnlock(&mutex) == ULT_OK, "mutex unlock failed"); + const int expected = rejected ? ULT_OK : ULT_ERROR_STATE; + if (outcome != ULT_OK || semaphoreResult != expected || mutexResult != expected) { + std::fprintf(stderr, "budget=%d rejected=%d attempts=%u semaphore=%d mutex=%d\n", + budget, rejected, allocationAttempts, semaphoreResult, mutexResult); + ++failures; + } + if (rejected) Require(sceUltFinalize() == ULT_OK, "finalization retry failed"); + Require(sceUltSemaphoreTryAcquire(&semaphore, 1) == ULT_ERROR_STATE, "finalized semaphore is still live"); + Require(sceUltMutexTryLock(&mutex) == ULT_ERROR_STATE, "finalized mutex is still live"); + } + std::printf("finalization allocation budgets: 8 cases, %u rejections, %u incorrect lifecycles\n", rejectedCases, failures); + Require(rejectedCases > 0, "no allocation failure was injected"); + Require(rejectedCases < 8, "no finalization succeeded without a retry"); + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/tests/WindowsExceptions.cpp b/core/libs/tests/WindowsExceptions.cpp index c83acd49c2..5adb55cac4 100644 --- a/core/libs/tests/WindowsExceptions.cpp +++ b/core/libs/tests/WindowsExceptions.cpp @@ -56,6 +56,17 @@ static void TestTypeInfoVtables() { if (!upcastType(&multiple, &base, &object) || object != storage.data() + 16) throw std::runtime_error("multiple RTTI upcast failed"); object = storage.data(); if (catchType(&other, &single, &object, 0)) throw std::runtime_error("unrelated RTTI catch succeeded"); + TypeRecord shared {classTable + 2 * sizeof(void*), "6Shared"}; + SingleRecord publicPath {{singleTable + 2 * sizeof(void*), "10PublicPath"}, &shared}; + SingleRecord privatePath {{singleTable + 2 * sizeof(void*), "11PrivatePath"}, &shared}; + MultipleRecord mixed {{multipleTable + 2 * sizeof(void*), "5Mixed"}, 1, 2, {{&publicPath.type, 2}, {&privatePath.type, 16 << 8}}}; + MultipleRecord hidden {{multipleTable + 2 * sizeof(void*), "6Hidden"}, 0, 1, {{&privatePath.type, 0}, {}}}; + object = storage.data(); + if (catchType(&shared, &mixed, &object, 0) || object != storage.data()) throw std::runtime_error("RTTI catch of a base repeated through a private path succeeded"); + if (upcastType(&mixed, &shared, &object) || object != storage.data()) throw std::runtime_error("RTTI upcast to a base repeated through a private path succeeded"); + if (!catchType(&publicPath, &mixed, &object, 0) || object != storage.data()) throw std::runtime_error("RTTI catch of the public path failed"); + object = storage.data(); + if (catchType(&shared, &hidden, &object, 0)) throw std::runtime_error("RTTI catch of a privately inherited base succeeded"); } extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char*); diff --git a/core/libs/tests/WindowsSharedMapCost.cpp b/core/libs/tests/WindowsSharedMapCost.cpp new file mode 100644 index 0000000000..ce6c25b6e7 --- /dev/null +++ b/core/libs/tests/WindowsSharedMapCost.cpp @@ -0,0 +1,78 @@ +#define NOMINMAX +#include "prx/libc/include/WindowsMappings.hpp" +#include +#include +#include +#include +#include +#include + +namespace { +using Mappings = GuestArena::WindowsMappings; +constexpr std::size_t PageBytes = 16384; +constexpr int Rounds = 5; + +[[noreturn]] void Fail(const char* what) { + std::fprintf(stderr, "windows shared map cost: %s (error %lu)\n", what, GetLastError()); + std::exit(1); +} + +std::size_t MappedViews(const unsigned char* begin, std::size_t bytes) { + std::set bases; + for (auto cursor = begin; cursor < begin + bytes;) { + MEMORY_BASIC_INFORMATION info{}; + if (VirtualQuery(cursor, &info, sizeof(info)) != sizeof(info)) Fail("view query failed"); + if (info.Type == MEM_MAPPED) bases.insert(info.AllocationBase); + cursor = static_cast(info.BaseAddress) + info.RegionSize; + } + return bases.size(); +} + +struct Sample { + double mapMs = 0; + double releaseMs = 0; + std::size_t views = 0; +}; + +Sample MapOnce(std::size_t pages) { + const auto bytes = pages * PageBytes; + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, static_cast(static_cast(bytes) >> 32), static_cast(bytes), nullptr); + auto* memory = static_cast(Mappings::Get().Reserve(nullptr, bytes)); + if (section == nullptr || memory == nullptr) Fail("cannot allocate shared memory"); + const auto start = std::chrono::steady_clock::now(); + Mappings::Get().Map(memory, bytes, section, 0, PAGE_READWRITE); + Sample sample; + sample.mapMs = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + sample.views = MappedViews(memory, bytes); + memory[0] = 1; + memory[bytes - 1] = 1; + const auto released = std::chrono::steady_clock::now(); + Mappings::Get().Reset(memory, bytes); + sample.releaseMs = std::chrono::duration(std::chrono::steady_clock::now() - released).count(); + VirtualFree(memory, 0, MEM_RELEASE); + CloseHandle(section); + return sample; +} + +double Median(std::vector values) { + std::sort(values.begin(), values.end()); + return values[values.size() / 2]; +} + +} + +int main() { + for (const std::size_t pages : {std::size_t{256}, std::size_t{16384}, std::size_t{65536}}) { + std::vector map; + std::vector release; + std::size_t views = 0; + for (int round = 0; round < Rounds; ++round) { + const auto sample = MapOnce(pages); + map.push_back(sample.mapMs); + release.push_back(sample.releaseMs); + views = sample.views; + } + std::printf("shared map of %zu pages (%zu MiB): map median %.2f ms (%.2f us per page), release median %.2f ms, %zu views\n", pages, pages * PageBytes >> 20, Median(map), Median(map) * 1000.0 / static_cast(pages), Median(release), views); + } + return 0; +} diff --git a/core/libs/tests/WindowsWriteWatch.cpp b/core/libs/tests/WindowsWriteWatch.cpp index 7a42cdea83..65d98f68e2 100644 --- a/core/libs/tests/WindowsWriteWatch.cpp +++ b/core/libs/tests/WindowsWriteWatch.cpp @@ -535,6 +535,103 @@ void CheckComparedPagesReportedOnce() { } } +namespace { +void CheckSharedMapQueriesPerMapping() { + constexpr std::size_t Pages = 256; + constexpr std::size_t Span = Pages * 16384; + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Span, nullptr); + auto* memory = static_cast(Mappings::Get().Reserve(nullptr, Span)); + Require(section && memory, "cannot allocate shared test memory"); + const auto before = Mappings::MemoryQueries(); + Mappings::Get().Map(memory, Span, section, 0, PAGE_READWRITE); + const auto queries = Mappings::MemoryQueries() - before; + for (std::size_t page = 0; page < Pages; ++page) memory[page * 16384 + 8] = static_cast(page); + for (std::size_t page = 0; page < Pages; ++page) Require(memory[page * 16384 + 8] == static_cast(page), "a shared page lost its store"); + Mappings::Get().Reset(memory, Span); + VirtualFree(memory, 0, MEM_RELEASE); + CloseHandle(section); + Require(queries < Pages, ("a shared mapping queried the address space for every page: " + std::to_string(queries) + " queries for " + std::to_string(Pages) + " pages").c_str()); +} + +std::size_t MappedViews(const unsigned char* begin, std::size_t bytes) { + std::set bases; + for (auto cursor = begin; cursor < begin + bytes;) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(cursor, &info, sizeof(info)) == sizeof(info), "view query failed"); + if (info.Type == MEM_MAPPED) bases.insert(info.AllocationBase); + cursor = static_cast(info.BaseAddress) + info.RegionSize; + } + return bases.size(); +} + +DWORD ProtectionAt(const unsigned char* address) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(address, &info, sizeof(info)) == sizeof(info), "protection query failed"); + return info.State == MEM_COMMIT ? info.Protect : 0; +} + +void CheckSharedRunIsOneView() { + constexpr std::size_t Gib = std::size_t{1} << 30; + HANDLE large = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, static_cast(Gib), nullptr); + auto* whole = static_cast(Mappings::Get().Reserve(nullptr, Gib)); + Require(large && whole, "cannot allocate a shared gibibyte"); + Mappings::Get().Map(whole, Gib, large, 0, PAGE_READWRITE); + const auto wholeViews = MappedViews(whole, Gib); + whole[Gib - 1] = 1; + Mappings::Get().Reset(whole, Gib); + VirtualFree(whole, 0, MEM_RELEASE); + CloseHandle(large); + Require(wholeViews == 1, ("a shared gibibyte was mapped as " + std::to_string(wholeViews) + " views instead of one").c_str()); + + constexpr std::size_t Page = 16384; + constexpr std::size_t Pages = 64; + constexpr std::size_t Span = Pages * Page; + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Span + Page, nullptr); + auto* memory = static_cast(Mappings::Get().Reserve(nullptr, Span)); + auto* other = static_cast(Mappings::Get().Reserve(nullptr, Page)); + Require(section && memory && other, "cannot allocate shared test memory"); + Mappings::Get().Map(memory, Span, section, 0, PAGE_READWRITE); + for (std::size_t page = 0; page < Pages; ++page) memory[page * Page + 8] = static_cast(page + 1); + Require(MappedViews(memory, Span) == 1, "a shared run was not mapped as one view"); + Mappings::Get().SetProtection(reinterpret_cast(memory + 5 * Page), Page, PAGE_READONLY); + DWORD previous; + Require(VirtualProtect(memory + 5 * Page, Page, PAGE_READONLY, &previous), "guest protection of one page failed"); + Require(MappedViews(memory, Span) == 1, "a per-page protection split the run"); + Mappings::Get().Map(other, Page, section, 30 * Page, PAGE_READWRITE); + Require(MappedViews(memory, Span) == 1, "an alias of one page split the run"); + other[16] = 0x5a; + Require(memory[30 * Page + 16] == 0x5a, "an alias store did not reach the run"); + std::array collected{}; + std::size_t available = collected.size(); + Require(Mappings::Get().Collect(reinterpret_cast(memory + 50 * Page), Page, collected.data(), &available, true, Collection::All), "run collection failed"); + Require(ProtectionAt(memory + 50 * Page) == PAGE_READONLY, "collection did not arm the run's page"); + Mappings::Get().Reset(memory + 10 * Page, Page); + Require(MappedViews(memory, Span) == 2, ("a partial unmap left " + std::to_string(MappedViews(memory, Span)) + " views instead of two").c_str()); + Require(ProtectionAt(memory + 10 * Page) == 0, "the unmapped page is still mapped"); + Mappings::Get().Map(memory + 20 * Page, Page, section, Span, PAGE_READWRITE); + Require(MappedViews(memory, Span) == 4, ("a remap of one page left " + std::to_string(MappedViews(memory, Span)) + " views instead of four").c_str()); + Require(memory[20 * Page + 8] == 0, "the remapped page shows the old page"); + for (std::size_t page = 0; page < Pages; ++page) { + if (page == 10 || page == 20) continue; + Require(memory[page * Page + 8] == static_cast(page + 1), ("a split lost page " + std::to_string(page)).c_str()); + } + Require(ProtectionAt(memory + 5 * Page) == PAGE_READONLY && ProtectionAt(memory + 6 * Page) == PAGE_READWRITE, "a split lost a page protection"); + Require(ProtectionAt(memory + 50 * Page) == PAGE_READONLY, "a split dropped a write-tracking protection"); + const auto before = faults.load(); + memory[50 * Page + 8] = 0x77; + Require(faults == before + 1, "a write to an armed page of a split run did not fault"); + memory[51 * Page + 8] = 0x78; + available = collected.size(); + Require(Mappings::Get().Collect(reinterpret_cast(memory + 50 * Page), 2 * Page, collected.data(), &available, true, Collection::Armed), "armed collection failed"); + Require(available == 1 && collected[0] == memory + 50 * Page, "the split run lost a tracked write"); + Mappings::Get().Reset(memory, Span); + Mappings::Get().Reset(other, Page); + VirtualFree(memory, 0, MEM_RELEASE); + VirtualFree(other, 0, MEM_RELEASE); + CloseHandle(section); +} +} + int main() { auto* handler = AddVectoredExceptionHandler(1, HandleWrite); try { @@ -547,6 +644,8 @@ int main() { CheckPrivate(); CheckConcurrentWrites(); CheckForgottenSectionKeepsNoPages(); + CheckSharedMapQueriesPerMapping(); + CheckSharedRunIsOneView(); CheckComparedWhenShared(); CheckResidentOnlyComparedWhenShared(); CheckComparedWhenSharedRemapped(); diff --git a/core/libs/tests/test_public_headers.py b/core/libs/tests/test_public_headers.py index 8aa05f692b..2511659765 100644 --- a/core/libs/tests/test_public_headers.py +++ b/core/libs/tests/test_public_headers.py @@ -5,6 +5,7 @@ import json import re import subprocess import sys +import tempfile from functools import cache from pathlib import Path @@ -171,12 +172,30 @@ def compile_headers(compiler, compiler_args, groups, workers=4): def compile_one(group): includes = "".join(f'#include "{header.relative_to(LIBS).as_posix()}"\n' for header in group) - result = subprocess.run([compiler, *compiler_args, "-std=c++20", *warnings, "-fsyntax-only", "-x", "c++", f"-I{LIBS}", f"-I{ROOT / '3rdparty/freetype/include'}", "-"], input=includes * 2, text=True, capture_output=True) - if result.returncode: - raise RuntimeError(f"Header compilation failed: {group}\n{result.stdout}{result.stderr}") + with tempfile.TemporaryDirectory() as directory: + dependencies = Path(directory) / "headers.d" + result = subprocess.run([compiler, *compiler_args, "-std=c++20", *warnings, "-fsyntax-only", "-MD", "-MF", str(dependencies), "-MT", "public_headers", "-x", "c++", f"-I{LIBS}", f"-I{ROOT / '3rdparty/freetype/include'}", "-"], input=includes * 2, text=True, capture_output=True) + if result.returncode: + raise RuntimeError(f"Header compilation failed: {group}\n{result.stdout}{result.stderr}") + text = dependencies.read_text(encoding="utf-8").replace("\\\n", "") + if not text.startswith("public_headers:"): + raise RuntimeError(f"Invalid compiler dependency output: {dependencies}") + names = re.findall(r"(?:\\[ \t]|[^\s])+", text.removeprefix("public_headers:")) + paths = [Path(re.sub(r"\\([ \t#\\])", r"\1", name).replace("$$", "$")).resolve() for name in names] + forbidden = [path for path in paths if path.is_relative_to((ROOT / "3rdparty").resolve())] + if forbidden: + raise RuntimeError(f"Public headers depend on 3rdparty: {group}\n" + "\n".join(map(str, forbidden))) + errors = [] with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool: - list(pool.map(compile_one, groups)) + futures = [(group, pool.submit(compile_one, group)) for group in groups] + for group, future in futures: + try: + future.result() + except Exception as error: + errors.append(f"{group}: {error}") + if errors: + raise RuntimeError(f"{len(errors)} header compilation units failed:\n\n" + "\n\n".join(errors)) def main(): diff --git a/core/libs/tests/test_windows_timed_join_imports.py b/core/libs/tests/test_windows_timed_join_imports.py new file mode 100644 index 0000000000..4a5eeef09c --- /dev/null +++ b/core/libs/tests/test_windows_timed_join_imports.py @@ -0,0 +1,86 @@ +import os +import shutil +import struct +import subprocess +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / 'relinker' / 'relinker' / 'tests')) +from guest_import_fixtures import executable, fixture_directory + + +def call(code, slot): + code.extend(b'\xff\x15' + struct.pack(' ${group}) @@ -564,6 +574,8 @@ if(BUILD_TESTING) set_tests_properties(macos_tls_alu PROPERTIES TIMEOUT 60) add_test(NAME linux_load_alignment COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_load_alignment.py $) + add_test(NAME linux_load_bounds COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_load_bounds.py $) add_test(NAME linux_eh_frame_header COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_eh_frame_header.py $) add_test(NAME linux_dynamic_strings COMMAND ${Python3_EXECUTABLE} @@ -576,6 +588,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_executable_exports.py $) add_test(NAME executable_tls_relocations COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executable_tls_relocations.py $) + add_test(NAME executable_export_bounds COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executable_export_bounds.py $) add_test(NAME macos_empty_tls COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_empty_tls.py $) set_tests_properties(macos_empty_tls PROPERTIES TIMEOUT 60) @@ -598,12 +612,17 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_trace_imports.py $) add_test(NAME linux_trace_imports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_trace_imports.py $) + add_test(NAME macos_trace_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_trace_imports.py $) add_test(NAME windows_address_space COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_address_space.py $) add_test(NAME windows_tls_thread_pointer COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_tls_thread_pointer.py $) add_test(NAME windows_bss COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_bss.py $) + add_test(NAME windows_lazy_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_lazy_imports.py $) + set_tests_properties(windows_lazy_imports PROPERTIES TIMEOUT 30) add_test(NAME windows_sparse_load COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_sparse_load.py $) add_test(NAME windows_icon COMMAND ${Python3_EXECUTABLE} @@ -616,12 +635,17 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_syscall_trap.py $) add_test(NAME guest_tls COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_tls.py $) + add_test(NAME guest_tls_export_references COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_tls_export_references.py $) + set_tests_properties(guest_tls_export_references PROPERTIES TIMEOUT 30) add_test(NAME guest_needed_modules COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_modules.py $) add_test(NAME guest_module_dir COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_dir.py $) add_test(NAME guest_module_directories COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_directories.py $) + add_test(NAME guest_trailing_note COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_trailing_note.py $) add_test(NAME relinker_executables COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executables.py $) add_test(NAME game_patches COMMAND ${Python3_EXECUTABLE} @@ -639,6 +663,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_guest_stack.py $) add_test(NAME guest_symbol_names COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_symbol_names.py $) + add_test(NAME import_symbol_types COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_import_symbol_types.py $) add_test(NAME guest_registry COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_registry.py $) add_test(NAME needed_libraries COMMAND ${Python3_EXECUTABLE} @@ -717,7 +743,7 @@ if(BUILD_TESTING) set(msvcrtImportLibrary "${CMAKE_CURRENT_BINARY_DIR}/windows_stdio_msvcrt_import.a") add_custom_command(OUTPUT "${msvcrtImportLibrary}" COMMAND "${mingwDlltoolExecutable}" -d "${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/WindowsMsvcrt.def" - -l "${msvcrtImportLibrary}" -D msvcrt.dll + -l "${msvcrtImportLibrary}" -D msvcrt.dll --temp-prefix windows_stdio_msvcrt_import DEPENDS elfpatcher/tests/WindowsMsvcrt.def VERBATIM) add_custom_target(windows_stdio_msvcrt_import DEPENDS "${msvcrtImportLibrary}") @@ -753,6 +779,7 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_guest_weak_imports.py $) add_test(NAME input_magic COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_magic.py $) + add_asan_test_options(input_magic max_allocation_size_mb=512) add_test(NAME input_preservation COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_preservation.py $) add_test(NAME output_collisions COMMAND ${Python3_EXECUTABLE} @@ -782,6 +809,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_call_registry.py $) add_test(NAME jmprel_table_bounds COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_jmprel_table_bounds.py $) + add_test(NAME copy_relocations COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_copy_relocations.py $) if(WIN32) add_test(NAME windows_diagnostic_fixture_isolation COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/test_windows_diagnostic_fixture_isolation.py $) diff --git a/core/relinker/cli/include/NativeModules.hpp b/core/relinker/cli/include/NativeModules.hpp index 66139266ca..862acfcf0c 100644 --- a/core/relinker/cli/include/NativeModules.hpp +++ b/core/relinker/cli/include/NativeModules.hpp @@ -1,11 +1,13 @@ #pragma once #include +#include namespace Cli { struct NativeModules { std::filesystem::path Root; + std::vector Directories; std::vector RuntimeDirectories; std::vector Modules; }; @@ -14,4 +16,6 @@ bool SameDirectory(const std::filesystem::path& first, const std::filesystem::pa NativeModules ReadModules(const std::filesystem::path& inputPath, const std::filesystem::path& sceModulePath, const std::set& excludedModules, const std::vector& moduleDirectories); +void ReportUnconvertedNeeded(const NativeModules& modules, const std::vector& neededLibraries, bool foldCase, std::ostream& out); + } diff --git a/core/relinker/cli/src/Autorun.cpp b/core/relinker/cli/src/Autorun.cpp index aa54150b47..e6e6defd69 100644 --- a/core/relinker/cli/src/Autorun.cpp +++ b/core/relinker/cli/src/Autorun.cpp @@ -16,11 +16,10 @@ namespace Cli { int Autorun(const std::string& absPath, bool toWindows) { if (!toWindows) { - std::filesystem::permissions(Io::NativePath(absPath), - std::filesystem::perms::owner_exec | - std::filesystem::perms::group_exec | - std::filesystem::perms::others_exec, - std::filesystem::perm_options::add); + const auto path = Io::NativePath(absPath); + constexpr auto executable = std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec; + if ((std::filesystem::status(path).permissions() & executable) != executable) + std::filesystem::permissions(path, executable, std::filesystem::perm_options::add); } int rawCode = 0; diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index 47166d9972..f117c9e040 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -22,7 +22,7 @@ namespace { constexpr std::string_view kVerifyRejectedOptions[] = { "--windows", "--macos", "--windows-gui", "--windows-diagnostics", "--windows-fixed-base", "--trace-imports", "--linux-fixed-base", "--macos-fixed-base", - "--to-intel", "--to-rosetta", "--skip-syscall-check", "--trap-sdk-syscall-stubs", "unused-filter=", + "--to-intel", "--to-rosetta", "--frame-red-zone", "--skip-syscall-check", "--trap-sdk-syscall-stubs", "unused-filter=", "--rpath", "--registry", "--lazy-binding", "--autorun", "--patches", "--no-patches", }; @@ -199,8 +199,6 @@ Args ParseArgs(int argc, char* argv[]) { if (args.windowsFixedBase && !args.toWindows) throw std::runtime_error("--windows-fixed-base requires --windows"); - if (args.traceImports && args.toMacos) - throw std::runtime_error("--trace-imports cannot be used with --macos"); if (args.traceImports && args.lazyBinding) throw std::runtime_error("--trace-imports conflicts with --lazy-binding"); if (args.linuxFixedBase && (args.toWindows || args.toMacos)) diff --git a/core/relinker/cli/src/NativeModules.cpp b/core/relinker/cli/src/NativeModules.cpp index f01ac374d4..6163063c69 100644 --- a/core/relinker/cli/src/NativeModules.cpp +++ b/core/relinker/cli/src/NativeModules.cpp @@ -2,13 +2,59 @@ #include #include #include +#include #include +#include #include #include #include +#include +#include +#include +#include namespace Cli { +namespace { + +struct CandidateHeader { + bool Elf = false; + bool Self = false; + std::uint16_t Type = 0; +}; + +std::optional TryReadCandidateHeader(const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + if (!stream) return std::nullopt; + char header[18]{}; + stream.read(header, sizeof header); + if (stream.bad()) return std::nullopt; + CandidateHeader result; + if (stream.gcount() < 4) return result; + const auto byte = [&](const std::size_t index) { return static_cast(header[index]); }; + result.Self = (byte(0) == 0x4f && byte(1) == 0x15 && byte(2) == 0x3d && byte(3) == 0x1d) || (byte(0) == 0x54 && byte(1) == 0x14 && byte(2) == 0xf5 && byte(3) == 0xee); + result.Elf = byte(0) == 0x7f && header[1] == 'E' && header[2] == 'L' && header[3] == 'F'; + if (result.Elf && stream.gcount() == sizeof header) result.Type = static_cast(byte(16) | (byte(17) << 8)); + return result; +} + +CandidateHeader ReadCandidateHeader(const std::filesystem::path& path) { + const auto header = TryReadCandidateHeader(path); + if (!header) throw Domain::RelinkerException("Cannot read guest candidate: " + Io::Utf8Path(path)); + return *header; +} + +std::string FoldedName(std::string name, const bool foldCase) { + if (foldCase) for (auto& character : name) if (character >= 'A' && character <= 'Z') character = static_cast(character + ('a' - 'A')); + return name; +} + +std::string_view ModuleStem(const std::string_view name) { + return name.substr(0, name.size() - Domain::ModuleSuffix(name).size()); +} + +} + bool SameDirectory(const std::filesystem::path& first, const std::filesystem::path& second) { if (std::filesystem::exists(first) && std::filesystem::exists(second)) return std::filesystem::equivalent(first, second); return std::filesystem::weakly_canonical(first) == std::filesystem::weakly_canonical(second); @@ -45,17 +91,6 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil } std::vector paths; std::set unmatchedExclusions = excludedModules; - const auto isElf = [](const std::filesystem::path& path, bool& self) { - std::ifstream stream(path, std::ios::binary); - if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + Io::Utf8Path(path)); - char magic[4]{}; - stream.read(magic, 4); - if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + Io::Utf8Path(path)); - if (stream.gcount() != 4) return false; - const auto byte = [&](const std::size_t index) { return static_cast(magic[index]); }; - self = (byte(0) == 0x4f && byte(1) == 0x15 && byte(2) == 0x3d && byte(3) == 0x1d) || (byte(0) == 0x54 && byte(1) == 0x14 && byte(2) == 0xf5 && byte(3) == 0xee); - return byte(0) == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F'; - }; for (const auto& directory : directories) { if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + Io::Utf8Path(directory)); for (const auto& entry : std::filesystem::directory_iterator(directory)) { @@ -66,9 +101,9 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil } if (!entry.is_regular_file()) continue; if (Io::SameFile(entry.path(), inputPath)) continue; - bool self = false; - if (isElf(entry.path(), self)) paths.push_back(entry.path()); - else if (self) + const auto header = ReadCandidateHeader(entry.path()); + if (header.Elf) paths.push_back(entry.path()); + else if (header.Self) std::cout << "Skipping SELF container: " << entry.path().lexically_relative(root).generic_string() << " (unwrap it before relinking)\n"; } @@ -77,6 +112,7 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil std::sort(paths.begin(), paths.end()); NativeModules result; result.Root = root; + result.Directories = std::move(directories); result.RuntimeDirectories = std::move(runtimeDirectories); for (const auto& path : paths) result.Modules.push_back({path, Io::FileReader().Read(Io::Utf8Path(path))}); std::map sonames; @@ -107,4 +143,49 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil return result; } +void ReportUnconvertedNeeded(const NativeModules& modules, const std::vector& neededLibraries, const bool foldCase, std::ostream& out) { + std::map> stems; + for (const auto& name : neededLibraries) { + const auto folded = FoldedName(name, foldCase); + if (!Domain::ModuleSuffix(folded).empty()) stems[std::string(ModuleStem(folded))].push_back(name); + } + if (stems.empty()) return; + struct Found { + std::filesystem::path Relative; + std::string Name; + bool Self; + }; + std::vector found; + std::error_code error; + for (auto entry = std::filesystem::recursive_directory_iterator(modules.Root, std::filesystem::directory_options::skip_permission_denied, error); !error && entry != std::filesystem::recursive_directory_iterator(); entry.increment(error)) { + const bool directory = entry->is_directory(error); + if (error) { + error.clear(); + continue; + } + if (directory) { + if (std::any_of(modules.Directories.begin(), modules.Directories.end(), [&](const auto& scanned) { return std::filesystem::equivalent(scanned, entry->path(), error) && !error; })) entry.disable_recursion_pending(); + error.clear(); + continue; + } + const auto filename = FoldedName(Io::Utf8Path(entry->path().filename()), foldCase); + if (Domain::ModuleSuffix(filename).empty()) continue; + const auto stem = stems.find(std::string(ModuleStem(filename))); + if (stem == stems.end()) continue; + if (!entry->is_regular_file(error) || error) { + error.clear(); + continue; + } + const auto header = TryReadCandidateHeader(entry->path()); + if (!header || (!header->Self && !(header->Elf && header->Type == 0xfe18))) continue; + for (const auto& name : stem->second) found.push_back({entry->path().lexically_relative(modules.Root), name, header->Self}); + } + std::sort(found.begin(), found.end(), [](const auto& left, const auto& right) { return std::tie(left.Relative, left.Name) < std::tie(right.Relative, right.Name); }); + for (const auto& [relative, name, self] : found) { + const auto directory = relative.parent_path().generic_string(); + out << "Needed module not converted: " << name << " at " << relative.generic_string() << "; add --module-dir " << (directory.empty() ? "." : directory) + << (self ? " and unwrap the SELF container" : "") << '\n'; + } +} + } diff --git a/core/relinker/cli/tests/AutorunExitTests.cpp b/core/relinker/cli/tests/AutorunExitTests.cpp index c773826ee9..798fb129db 100644 --- a/core/relinker/cli/tests/AutorunExitTests.cpp +++ b/core/relinker/cli/tests/AutorunExitTests.cpp @@ -3,7 +3,11 @@ #include #include #include +#ifndef _WIN32 +#include +#endif #include +#include #include #include #include @@ -40,26 +44,43 @@ int main(int, char* argv[]) { std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + " with spaces" + executable.extension().string()); #ifndef _WIN32 const auto signaled = child.parent_path() / ("anyps5-autorun-child-signal-" + child.filename().string()); + const auto script = child.parent_path() / ("anyps5-autorun-script-" + child.filename().string()); + const auto changeTime = [](const std::filesystem::path& path) { + struct stat info {}; + if (stat(path.c_str(), &info) != 0) throw std::runtime_error("Cannot stat " + path.string()); +#ifdef __APPLE__ + return info.st_ctimespec.tv_sec * 1000000000LL + info.st_ctimespec.tv_nsec; +#else + return info.st_ctim.tv_sec * 1000000000LL + info.st_ctim.tv_nsec; +#endif + }; #endif std::istringstream input("\n\n\n"); auto* savedInput = std::cin.rdbuf(input.rdbuf()); int result = 0; + std::vector aliases; try { +#ifdef _WIN32 std::filesystem::copy_file(executable, child); +#else + std::filesystem::create_symlink(executable, child); + const auto changed = changeTime(executable); +#endif std::vector suffixes{""}; #ifndef _WIN32 suffixes.insert(suffixes.end(), {" $HOME", " $(printf substituted)", " `printf substituted`", " 'single'", " \"double\"", " \\backslash", " line\nbreak"}); #endif - const auto originalChild = child; for (const auto& suffix : suffixes) { - const auto renamed = std::filesystem::path(originalChild.string() + suffix); - if (renamed != child) std::filesystem::rename(child, renamed); - child = renamed; + const auto alias = std::filesystem::path(child.string() + suffix); + if (alias != child) { + std::filesystem::create_symlink(child, alias); + aliases.push_back(alias); + } for (const bool windows : {false, true}) { - const int code = Cli::Autorun(child.string(), windows); + const int code = Cli::Autorun(alias.string(), windows); if (code != 42) throw std::runtime_error("Autorun changed exit code 42 to " + - std::to_string(code) + " for " + child.string()); + std::to_string(code) + " for " + alias.string()); } } #ifndef _WIN32 @@ -85,18 +106,24 @@ int main(int, char* argv[]) { } if (std::signal(SIGCHLD, savedHandler) == SIG_ERR) throw std::runtime_error("Cannot restore SIGCHLD after the autorun failure test"); - std::filesystem::rename(child, signaled); + std::filesystem::create_symlink(child, signaled); const int code = Cli::Autorun(signaled.string(), false); if (code != 128 + SIGILL) throw std::runtime_error("Autorun lost the child signal"); + if (changeTime(executable) != changed) throw std::runtime_error("Autorun changed the mode of an output that was already executable"); + std::ofstream(script) << "#!/bin/sh\nexit 42\n"; + std::filesystem::permissions(script, std::filesystem::perms::owner_read | std::filesystem::perms::owner_write); + if (Cli::Autorun(script.string(), false) != 42) throw std::runtime_error("Autorun did not make a non-executable output executable"); #endif } catch (const std::exception& error) { std::cerr << error.what() << '\n'; result = 1; } std::cin.rdbuf(savedInput); + for (const auto& alias : aliases) std::filesystem::remove(alias); std::filesystem::remove(child); #ifndef _WIN32 std::filesystem::remove(signaled); + std::filesystem::remove(script); #endif return result; } diff --git a/core/relinker/cli/tests/CliArgsTests.cpp b/core/relinker/cli/tests/CliArgsTests.cpp index 8f18c4c963..3cd3069dc1 100644 --- a/core/relinker/cli/tests/CliArgsTests.cpp +++ b/core/relinker/cli/tests/CliArgsTests.cpp @@ -322,7 +322,7 @@ void verifyMode() { expectThrows({"--verify-patches", "p.toml", "--verify-output", "out.elf", "--exclude-sce-module", "x.prx"}, noEffect); expectThrows({"--verify-patches", "p.toml", "--verify-output", "out.elf", "--skip-sce-module"}, noEffect); expectThrows({"--verify-patches", "p.toml", "in.elf", "extra"}, "unexpected argument: extra"); - for (const auto* option : {"--windows", "--macos", "--to-intel", "--to-rosetta", "--trace-imports", "--lazy-binding", "--autorun", "--registry", "--skip-syscall-check", "--no-patches"}) + for (const auto* option : {"--windows", "--macos", "--to-intel", "--to-rosetta", "--frame-red-zone", "--trace-imports", "--lazy-binding", "--autorun", "--registry", "--skip-syscall-check", "--no-patches"}) expectThrows({"--verify-patches", "p.toml", option, "in.elf"}, std::string(option) + " cannot be used with --verify-patches"); expectThrows({"--verify-patches", "p.toml", "--rpath", "x", "in.elf"}, "--rpath cannot be used with --verify-patches"); expectThrows({"--verify-patches", "p.toml", "--patches", "q.toml", "in.elf"}, "--patches cannot be used with --verify-patches"); diff --git a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp index c6db778a9e..e2c581eb24 100644 --- a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp +++ b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp @@ -43,6 +43,7 @@ inline constexpr std::uint8_t OneByteArpl = 0x62; inline constexpr std::uint8_t OneByteMovsxd = 0x63; inline constexpr std::uint8_t OneByteModRmRangeIMin = 0x84; inline constexpr std::uint8_t OneByteModRmRangeIMax = 0x8F; +inline constexpr std::uint8_t OneByteLea = 0x8D; inline constexpr std::uint8_t OneByteGrp2Rm8Imm8 = 0xC0; inline constexpr std::uint8_t OneByteGrp2RmImm8 = 0xC1; inline constexpr std::uint8_t OneByteVex3 = 0xC4; diff --git a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp index 065e2ad80e..ecbe487147 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -201,6 +201,8 @@ private: Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchMovnts(const DecodedInstruction& instr, const Entry& entry) const { using namespace X64OpcodeConstants; const auto opcodeOffset = instr.OpcodeOffset(); + if (std::find(instr.Data, instr.Data + opcodeOffset, PrefixLock) != instr.Data + opcodeOffset) + throw CodegenException("MOVNTSS/MOVNTSD with a LOCK prefix"); if (opcodeOffset + 2 >= instr.Length) throw CodegenException("MOVNTSS/MOVNTSD truncated before its ModRM byte"); const auto modrm = instr.Data[opcodeOffset + 2]; diff --git a/core/relinker/codegen/src/x86/RedZoneFramer.cpp b/core/relinker/codegen/src/x86/RedZoneFramer.cpp index cfe714faef..5536565d71 100644 --- a/core/relinker/codegen/src/x86/RedZoneFramer.cpp +++ b/core/relinker/codegen/src/x86/RedZoneFramer.cpp @@ -78,6 +78,11 @@ struct MemoryUse { bool Address = false; }; +bool xopPrefixed(const std::uint8_t* code, const Insn& insn) { + const auto at = insn.Info.OpcodeOffset; + return code[at] == 0x8f && at + 1 < insn.Length && (code[at + 1] & 0x1f) >= 8; +} + void extensionBits(const std::uint8_t* code, const Insn& insn, bool& r, bool& x, bool& b) { const auto& info = insn.Info; r = x = b = false; @@ -88,7 +93,7 @@ void extensionBits(const std::uint8_t* code, const Insn& insn, bool& r, bool& x, return; } const auto lead = code[info.OpcodeOffset]; - if ((lead == 0xc4 || lead == 0x8f || lead == 0x62) && info.OpcodeOffset + 1 < insn.Length) { + if ((lead == 0xc4 || lead == 0x62 || xopPrefixed(code, insn)) && info.OpcodeOffset + 1 < insn.Length) { r = (code[info.OpcodeOffset + 1] & 0x80) == 0; x = (code[info.OpcodeOffset + 1] & 0x40) == 0; b = (code[info.OpcodeOffset + 1] & 0x20) == 0; @@ -128,7 +133,7 @@ MemoryUse memoryUse(const std::uint8_t* code, const Insn& insn) { const auto mod = static_cast(modrm >> 6); const auto reg = static_cast((modrm >> 3) & 7); const auto rm = static_cast(modrm & 7); - const bool vex = info.RexPrefix == 0 && (code[info.OpcodeOffset] == 0xc4 || code[info.OpcodeOffset] == 0xc5 || code[info.OpcodeOffset] == 0x62 || code[info.OpcodeOffset] == 0x8f); + const bool vex = info.RexPrefix == 0 && (code[info.OpcodeOffset] == 0xc4 || code[info.OpcodeOffset] == 0xc5 || code[info.OpcodeOffset] == 0x62 || xopPrefixed(code, insn)); if (!vex && !info.IsTwoByteOpcode && reg == kRegRsp && !r) { const auto op = info.Opcode; if ((op <= 0x3f && (op & 7) <= 3) || (op >= 0x84 && op <= 0x8b) || op == 0x8d) use.UsesRsp = use.RegisterRsp = true; diff --git a/core/relinker/codegen/src/x86/Sha1Operands.cpp b/core/relinker/codegen/src/x86/Sha1Operands.cpp index 69ea10a68a..aa49ed27d5 100644 --- a/core/relinker/codegen/src/x86/Sha1Operands.cpp +++ b/core/relinker/codegen/src/x86/Sha1Operands.cpp @@ -20,16 +20,18 @@ Sha1Operands DecodeSha1(const std::uint8_t* data, const std::size_t length) { pos += 1; continue; } + if (b == PrefixLock) { + throw CodegenException("SHA-1 instruction with a LOCK prefix"); + } if (b == PrefixOperandSize || b == PrefixRepne || b == PrefixRep) { throw CodegenException("Not a SHA-1 instruction"); } - if (b != PrefixLock && b != PrefixAddressSize && + if (b != PrefixAddressSize && b != PrefixSegCs && b != PrefixSegSs && b != PrefixSegDs && b != PrefixSegEs && b != PrefixSegFs && b != PrefixSegGs) { break; } - if (b != PrefixLock) - prefixes.push_back(b); + prefixes.push_back(b); rex = 0; pos += 1; } diff --git a/core/relinker/codegen/src/x86/Sha256Operands.cpp b/core/relinker/codegen/src/x86/Sha256Operands.cpp index a090514a15..51af8ec376 100644 --- a/core/relinker/codegen/src/x86/Sha256Operands.cpp +++ b/core/relinker/codegen/src/x86/Sha256Operands.cpp @@ -20,16 +20,18 @@ Sha256Operands DecodeSha256(const std::uint8_t* data, const std::size_t length) pos += 1; continue; } + if (b == PrefixLock) { + throw CodegenException("SHA-256 instruction with a LOCK prefix"); + } if (b == PrefixOperandSize || b == PrefixRepne || b == PrefixRep) { throw CodegenException("Not a SHA-256 instruction"); } - if (b != PrefixLock && b != PrefixAddressSize && + if (b != PrefixAddressSize && b != PrefixSegCs && b != PrefixSegSs && b != PrefixSegDs && b != PrefixSegEs && b != PrefixSegFs && b != PrefixSegGs) { break; } - if (b != PrefixLock) - prefixes.push_back(b); + prefixes.push_back(b); rex = 0; pos += 1; } diff --git a/core/relinker/codegen/src/x86/Sse4aOperands.cpp b/core/relinker/codegen/src/x86/Sse4aOperands.cpp index cf492728c2..2a664f1140 100644 --- a/core/relinker/codegen/src/x86/Sse4aOperands.cpp +++ b/core/relinker/codegen/src/x86/Sse4aOperands.cpp @@ -19,11 +19,14 @@ Sse4aOperands DecodeSse4a(const std::uint8_t* data, const std::size_t length) { pos += 1; continue; } + if (b == PrefixLock) { + throw CodegenException("SSE4a instruction with a LOCK prefix"); + } if (b == PrefixOperandSize) { operandSizeOverride = true; } else if (b == PrefixRepne) { repnePrefix = true; - } else if (b != PrefixLock && b != PrefixRep && b != PrefixAddressSize && + } else if (b != PrefixRep && b != PrefixAddressSize && b != PrefixSegCs && b != PrefixSegSs && b != PrefixSegDs && b != PrefixSegEs && b != PrefixSegFs && b != PrefixSegGs) { break; diff --git a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp index 0c2f5571c9..78936363b4 100644 --- a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp @@ -624,6 +624,9 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( (op >= 0xaa && op <= 0xaf) || op == 0xd7)) info.HasMemoryOperand = true; + if ((!twoByteOpcode && op == OneByteLea) || (twoByteOpcode && op == TwoByteNopModRm)) + info.HasMemoryOperand = false; + info.FlowKind = ControlFlowKind::Sequential; return info; } diff --git a/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp b/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp index 82990dd9ac..e17dde61e9 100644 --- a/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp @@ -54,6 +54,12 @@ std::int64_t X64InstructionRewriter::AdjustedDelta( std::int64_t sliceSize, std::int64_t delta ) { + const bool targetInsideCut = targetPosition > cutStart && targetPosition < cutEnd; + + if (targetInsideCut) { + throw CodegenException("Branch target inside rewritten instruction", static_cast(instrPosition)); + } + const bool instrShifts = instrPosition >= cutEnd && instrPosition < sliceSize; const bool targetShifts = targetPosition >= cutEnd && targetPosition < sliceSize; diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index ca2cf46715..998fc11443 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -208,6 +209,8 @@ void decoderTwoByteOpcodeLengths() { } void sse4aOperands() { + requireFailure([] { const Bytes bytes = {0xF0, 0x66, 0x0F, 0x78, 0xC0, 0x08, 0x00}; (void)Codegen::DecodeSse4a(bytes.data(), bytes.size()); }, "LOCK EXTRQ was accepted"); + requireFailure([] { const Bytes bytes = {0xF2, 0xF0, 0x0F, 0x79, 0xC1}; (void)Codegen::DecodeSse4a(bytes.data(), bytes.size()); }, "LOCK INSERTQ was accepted"); const auto check = [](const Bytes& site, const bool insertq, const int dst, const int src, const int length, const int index) { const auto operands = Codegen::DecodeSse4a(site.data(), site.size()); require(operands.Insertq == insertq && !operands.RegisterForm && operands.Destination == dst && operands.Source == src && operands.Length == length && operands.Index == index, "SSE4a operands were decoded incorrectly"); @@ -231,6 +234,8 @@ void sse4aOperands() { } void sha256Operands() { + requireFailure([] { const Bytes bytes = {0xF0, 0x0F, 0x38, 0xCB, 0xCA}; (void)Codegen::DecodeSha256(bytes.data(), bytes.size()); }, "LOCK SHA256RNDS2 was accepted"); + requireFailure([] { const Bytes bytes = {0x2E, 0xF0, 0x0F, 0x38, 0xCC, 0x08}; (void)Codegen::DecodeSha256(bytes.data(), bytes.size()); }, "LOCK SHA256MSG1 memory form was accepted"); const auto check = [](const Bytes& site, const Codegen::Sha256Operation operation, const int dst, const int src) { const auto operands = Codegen::DecodeSha256(site.data(), site.size()); require(operands.Operation == operation && operands.Destination == dst && operands.Source == src, "SHA-256 operands were decoded incorrectly"); @@ -261,6 +266,8 @@ void sha256Operands() { } void sha1Operands() { + requireFailure([] { const Bytes bytes = {0xF0, 0x0F, 0x3A, 0xCC, 0xCA, 0x00}; (void)Codegen::DecodeSha1(bytes.data(), bytes.size()); }, "LOCK SHA1RNDS4 was accepted"); + requireFailure([] { const Bytes bytes = {0x2E, 0xF0, 0x0F, 0x38, 0xC9, 0x08}; (void)Codegen::DecodeSha1(bytes.data(), bytes.size()); }, "LOCK SHA1MSG1 memory form was accepted"); const auto check = [](const Bytes& site, const Codegen::Sha1Operation operation, const int dst, const int src, const int function) { const auto operands = Codegen::DecodeSha1(site.data(), site.size()); require(operands.Operation == operation && operands.Destination == dst && operands.Source == src && operands.Function == function, "SHA-1 operands were decoded incorrectly"); @@ -322,6 +329,8 @@ void matcherSubstitutions() { const auto movntsd = match({0xF2, 0x44, 0x0F, 0x2B, 0x4C, 0x24, 0x10}); require(movntsd && movntsd->Lowering == Codegen::Amd64OnlyLowering::InPlace && movntsd->ReplacementBytes == Bytes{0xF2, 0x44, 0x0F, 0x11, 0x4C, 0x24, 0x10} && movntsd->InstructionName == "MOVNTSD", "MOVNTSD was not rewritten to MOVSD"); requireFailure([&] { (void)match({0xF3, 0x0F, 0x2B, 0xC1}); }, "MOVNTSS with a register operand was accepted"); + const auto movntssDisplacement = match({0xF3, 0x0F, 0x2B, 0x47, 0xF0}); + require(movntssDisplacement && movntssDisplacement->ReplacementBytes == Bytes{0xF3, 0x0F, 0x11, 0x47, 0xF0}, "MOVNTSS displacement byte was treated as a LOCK prefix"); const auto monitorx = match({0x0F, 0x01, 0xFA}); require(monitorx && monitorx->Lowering == Codegen::Amd64OnlyLowering::InPlace && monitorx->ReplacementBytes == Bytes{0x0F, 0x1F, 0x00} && monitorx->InstructionName == "MONITORX", "MONITORX was not replaced by a NOP"); const auto mwaitx = match({0x0F, 0x01, 0xFB}); @@ -506,6 +515,13 @@ void converterSegment() { lockedRdpru[0x211] = 0x01; lockedRdpru[0x212] = 0xFD; requireFailure([&] { (void)converter->Convert(lockedRdpru, {segmentHeader(20)}); }, "LOCK RDPRU was accepted"); + for (const Bytes& instruction : {Bytes{0xF0, 0xF3, 0x0F, 0x2B, 0x07}, Bytes{0xF3, 0xF0, 0x0F, 0x2B, 0x07}, + Bytes{0xF0, 0xF2, 0x0F, 0x2B, 0x07}, Bytes{0xF2, 0xF0, 0x0F, 0x2B, 0x07}}) { + auto lockedMovnts = file; + std::copy(instruction.begin(), instruction.end(), lockedMovnts.begin() + 0x20F); + lockedMovnts[0x214] = 0xC3; + require(failureOffset([&] { (void)converter->Convert(lockedMovnts, {segmentHeader(21)}); }, "LOCK MOVNTSS/MOVNTSD was accepted") == 0x20F, "LOCK MOVNTSS/MOVNTSD failure does not carry the file offset"); + } auto registerForm = file; const Bytes extrqRegister = {0x66, 0x0F, 0x79, 0xCA}; std::copy(extrqRegister.begin(), extrqRegister.end(), registerForm.begin() + 0x20F); @@ -561,6 +577,9 @@ void converterSha256() { 0x90, 0xC3}; std::copy(text.begin(), text.end(), file.begin() + 0x200); + auto locked = file; + locked.insert(locked.begin() + 0x200, 0xF0); + require(failureOffset([&] { (void)converter->Convert(locked, {segmentHeader(text.size() + 1)}); }, "LOCK SHA-256 was accepted") == 0x200, "LOCK SHA-256 failure does not carry the file offset"); const auto result = converter->Convert(file, {segmentHeader(text.size())}); require(result.Trampolines.size() == 2 && result.Reports.size() == 2 && result.Bytes == file, "SHA-256 sites were not lowered through stubs"); const auto& rounds = result.Trampolines[0]; @@ -596,6 +615,9 @@ void converterSha1() { 0x90, 0xC3}; std::copy(text.begin(), text.end(), file.begin() + 0x200); + auto locked = file; + locked.insert(locked.begin() + 0x200, 0xF0); + require(failureOffset([&] { (void)converter->Convert(locked, {segmentHeader(text.size() + 1)}); }, "LOCK SHA-1 was accepted") == 0x200, "LOCK SHA-1 failure does not carry the file offset"); const auto result = converter->Convert(file, {segmentHeader(text.size())}); require(result.Trampolines.size() == 3 && result.Reports.size() == 3 && result.Bytes == file, "SHA-1 sites were not lowered through stubs"); require(result.Trampolines[0].Offset == 0x200 && result.Trampolines[0].Length == 5 && result.Reports[0].InstructionName == "SHA1RNDS4", "SHA1RNDS4 was not lowered as one site"); @@ -2059,6 +2081,130 @@ void xsaveoptExecution() { require(std::memcmp(area + 160 + 3 * 16, pattern, 16) == 0 && std::memcmp(area + 576 + 3 * 16, pattern + 16, 16) == 0, "XSAVE did not store YMM3 in the standard format"); require(std::memcmp(restored, pattern, sizeof(pattern)) == 0, "XRSTOR did not restore YMM3 from the area XSAVE wrote"); } + +void scalarCompareExecution() { + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes site = {0xF3, 0x0F, 0xC2, 0xC1, 0x05}; + const auto match = matcher->Match(site.data(), site.size()); + require(match && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline, "scalar compare stub was not produced"); + auto body = match->StubBody; + body[match->ReturnBranchOffset] = 0xC3; + void* code = mmap(nullptr, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + require(code != MAP_FAILED, "cannot map executable memory for the stub"); + std::memcpy(code, body.data(), body.size()); + struct Case { + float left; + float right; + std::uint32_t mask; + const char* name; + }; + for (const auto& item : std::vector{{309.0f, 309.0f, 0xFFFFFFFFu, "309 is not less than 309"}, + {309.0f, 310.0f, 0u, "309 is less than 310"}}) { + alignas(16) float left[4] = {item.left, 1.0f, 2.0f, 3.0f}; + alignas(16) float right[4] = {item.right, 0.0f, 0.0f, 0.0f}; + alignas(16) std::uint32_t result[4] = {}; + asm volatile( + "movaps (%[left]), %%xmm0\n\t" + "movaps (%[right]), %%xmm1\n\t" + "sub $128, %%rsp\n\t" + "call *%[code]\n\t" + "add $128, %%rsp\n\t" + "movaps %%xmm0, (%[result])\n\t" + : + : [left] "r"(left), [right] "r"(right), [result] "r"(result), [code] "r"(code) + : "memory", "xmm0", "xmm1"); + require(result[0] == item.mask, item.name); + require(result[1] == std::bit_cast(1.0f) && result[2] == std::bit_cast(2.0f) + && result[3] == std::bit_cast(3.0f), + "scalar compare stub changed the upper lanes of its destination"); + } + munmap(code, 4096); +} + +void rdpidExecution() { + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes site = {0xF3, 0x0F, 0xC7, 0xF8}; + const auto match = matcher->Match(site.data(), site.size()); + require(match && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline, "RDPID stub was not produced"); + auto body = match->StubBody; + body[match->ReturnBranchOffset] = 0xC3; + void* code = mmap(nullptr, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + require(code != MAP_FAILED, "cannot map executable memory for the stub"); + std::memcpy(code, body.data(), body.size()); + std::uint64_t processor = ~std::uint64_t{0}; + std::uint8_t carry = 0; + asm volatile( + "sub $128, %%rsp\n\t" + "stc\n\t" + "call *%[code]\n\t" + "setc %[carry]\n\t" + "add $128, %%rsp\n\t" + : "=&a"(processor), [carry] "=&q"(carry) + : [code] "r"(code) + : "memory", "cc"); + munmap(code, 4096); + require(processor == 0, "RDPID stub left a value other than processor 0 in RAX"); + require(carry == 1, "RDPID stub changed the flags"); +} + +void rdseedExecution() { + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes site = {0x0F, 0xC7, 0xF8}; + const auto match = matcher->Match(site.data(), site.size()); + require(match && match->Lowering == Codegen::Amd64OnlyLowering::InPlace, "RDSEED replacement was not produced"); + require(match->ReplacementBytes.size() == site.size(), "RDSEED replacement changed the instruction length"); + auto body = match->ReplacementBytes; + body.push_back(0xC3); + void* code = mmap(nullptr, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + require(code != MAP_FAILED, "cannot map executable memory for the replacement"); + std::memcpy(code, body.data(), body.size()); + bool produced = false; + for (unsigned attempt = 0; attempt < 16 && !produced; ++attempt) { + std::uint64_t value = 0; + std::uint8_t carry = 0; + asm volatile( + "sub $128, %%rsp\n\t" + "call *%[code]\n\t" + "setc %[carry]\n\t" + "add $128, %%rsp\n\t" + : "=&a"(value), [carry] "=&q"(carry) + : [code] "r"(code) + : "memory", "cc"); + produced = carry == 1; + } + munmap(code, 4096); + require(produced, "RDSEED replacement never reported a random value"); +} + +void clwbExecution() { + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes site = {0x66, 0x0F, 0xAE, 0x30}; + const auto match = matcher->Match(site.data(), site.size()); + require(match && match->Lowering == Codegen::Amd64OnlyLowering::InPlace, "CLWB replacement was not produced"); + require(match->ReplacementBytes.size() == site.size(), "CLWB replacement changed the instruction length"); + auto body = match->ReplacementBytes; + body.push_back(0xC3); + void* code = mmap(nullptr, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + require(code != MAP_FAILED, "cannot map executable memory for the replacement"); + std::memcpy(code, body.data(), body.size()); + std::uint8_t line[64] = {}; + std::uint64_t address = reinterpret_cast(line); + std::uint64_t kept = 0x5A5A5A5A5A5A5A5Aull; + std::uint8_t carry = 0; + asm volatile( + "sub $128, %%rsp\n\t" + "stc\n\t" + "call *%[code]\n\t" + "setc %[carry]\n\t" + "add $128, %%rsp\n\t" + : "+a"(address), "+b"(kept), [carry] "=&q"(carry) + : [code] "r"(code) + : "memory", "cc"); + munmap(code, 4096); + require(address == reinterpret_cast(line), "CLWB replacement changed its address register"); + require(kept == 0x5A5A5A5A5A5A5A5Aull, "CLWB replacement changed an unrelated register"); + require(carry == 1, "CLWB replacement changed the flags"); +} #else void reciprocalExecution() {} void registerFormExecution() {} @@ -2105,6 +2251,10 @@ void rdtscpExecution() { #else void rdtscpExecution() {} void wideStoreExecution() {} +void scalarCompareExecution() {} +void rdpidExecution() {} +void rdseedExecution() {} +void clwbExecution() {} #endif void scannerZeroTail() { @@ -2166,6 +2316,10 @@ int main() { converterRosettaLegacyScalarCompare(); rdtscpExecution(); wideStoreExecution(); + scalarCompareExecution(); + rdpidExecution(); + rdseedExecution(); + clwbExecution(); wideStoreSplit(); converterWideStoreWindows(); converterClaimedFollower(); diff --git a/core/relinker/codegen/tests/DecoderLengthTests.cpp b/core/relinker/codegen/tests/DecoderLengthTests.cpp index 3ad1299b0d..95c85168d7 100644 --- a/core/relinker/codegen/tests/DecoderLengthTests.cpp +++ b/core/relinker/codegen/tests/DecoderLengthTests.cpp @@ -120,6 +120,8 @@ int main() { for (const auto& testCase : std::vector>{ {"6590", false}, {"654889c8", false}, + {"658d0511111111", false}, + {"650f1f0511111111", false}, {"65488b0c2578030000", true}, {"65a4", true}, {"650fa2", false}, diff --git a/core/relinker/codegen/tests/RedZoneFramerTests.cpp b/core/relinker/codegen/tests/RedZoneFramerTests.cpp index 31466fcfdb..672eac6135 100644 --- a/core/relinker/codegen/tests/RedZoneFramerTests.cpp +++ b/core/relinker/codegen/tests/RedZoneFramerTests.cpp @@ -297,6 +297,31 @@ void skipsLeavesThatCopyRsp() { require(sites.empty() && displacements.empty() && report.Skipped.at("other use of rsp") == 2, "a leaf that copies rsp into another register was framed"); } +void tellsPopsWithoutRexFromXop() { + std::vector popBelowRsp = StoreBelowPushes; + append(popBelowRsp, {0x52, 0x8f, 0x44, 0x24, 0xf0}); + std::vector popRspWithFramePointer = StoreBelowPushes; + append(popRspWithFramePointer, {0x52, 0x8f, 0xc4}); + std::vector popRsp = StoreBelowRsp; + append(popRsp, {0x8f, 0xc4}); + const auto pops = image({leaf(popBelowRsp), leaf(popRspWithFramePointer), framelessLeaf(FramelessPrologue, popRsp, FramelessExit)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(pops.bytes, pops.segments, pops.functions, {}, report, displacements); + require(sites.empty() && displacements.empty() && report.RedZoneFunctions == 3, "a pop without REX to an rsp operand or into rsp, read as an XOP prefix, was framed"); + require(report.Skipped.at("other use of rsp") == 3, "a pop without REX to an rsp operand or into rsp was not reported as another use of rsp"); + + std::vector xopAtR12 = StoreBelowRsp; + append(xopAtR12, {0x8f, 0xc8, 0x78, 0xc0, 0x04, 0x24, 0x01}); + std::vector xopBelowRsp = StoreBelowRsp; + append(xopBelowRsp, {0x8f, 0xe8, 0x78, 0xc0, 0x44, 0x24, 0xf0, 0x01}); + const auto xops = image({framelessLeaf(FramelessPrologue, xopAtR12, FramelessExit), framelessLeaf(FramelessPrologue, xopBelowRsp, FramelessExit)}); + Codegen::RedZoneFrameReport xopReport; + std::vector xopDisplacements; + const auto xopSites = Codegen::FrameRedZoneFunctions(xops.bytes, xops.segments, xops.functions, {}, xopReport, xopDisplacements); + require(xopReport.Framed == 2 && xopReport.MovedOperands == 3 && xopReport.OperandStubs == 0 && xopSites.size() == 4, "an XOP operand was not read with the base register extension of its XOP prefix"); +} + const std::vector InflatePrologue{0x55, 0x41, 0x57, 0x41, 0x56, 0x41, 0x55, 0x41, 0x54, 0x53, 0x48, 0x83, 0xec, 0x40}; const std::vector InflatePops{0x5b, 0x41, 0x5c, 0x41, 0x5d, 0x41, 0x5e, 0x41, 0x5f, 0x5d, 0xc3}; const std::vector InflateFree{0x48, 0x83, 0xc4, 0x40}; @@ -616,6 +641,7 @@ int main() { keepsLoopsAlignedAfterAJump(); framesAPointerOnlyFunctionByItsOwnBounds(); leavesCodeWithoutAFunctionRange(); + tellsPopsWithoutRexFromXop(); std::cout << "red-zone framer tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/relinker/codegen/tests/RewriteBoundaryTests.cpp b/core/relinker/codegen/tests/RewriteBoundaryTests.cpp index dd1bd9a185..66a6ed2fc6 100644 --- a/core/relinker/codegen/tests/RewriteBoundaryTests.cpp +++ b/core/relinker/codegen/tests/RewriteBoundaryTests.cpp @@ -54,6 +54,16 @@ int main() { Require(result.Bytes == std::vector{ 0x90, 0x90, 0x90, 0xC3}); + bool targetInsideCutRejected = false; + try { + (void)rewriter.Rewrite( + {0xEB, 0x01, 0xB8, 0x11, 0x22, 0x33, 0x44, 0xC3}, + {2, {0x90}}); + } catch (const std::exception&) { + targetInsideCutRejected = true; + } + Require(targetInsideCutRejected); + for (const auto& code : { std::vector{ 0x66, 0xEB, 0x01, 0x90, 0xC3}, @@ -107,7 +117,7 @@ int main() { } RequireTruncatedBranchRejected(backwardWraparoundRejected); - std::cout << "21 rewrite boundary cases passed\n"; + std::cout << "22 rewrite boundary cases passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; diff --git a/core/relinker/domain/include/domain/Types.hpp b/core/relinker/domain/include/domain/Types.hpp index 6757f8507a..d08f990328 100644 --- a/core/relinker/domain/include/domain/Types.hpp +++ b/core/relinker/domain/include/domain/Types.hpp @@ -69,6 +69,7 @@ struct NidReference { std::int64_t Addend; bool Weak = false; bool Function = false; + std::uint8_t SymbolType = 0; }; struct CallSiteInfo { diff --git a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp index 99bc214e54..a93699a4bf 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp @@ -41,6 +41,12 @@ struct MacOsTlsExport { std::uint64_t Flags = 0; }; +struct MacOsTraceImport { + std::uint32_t Rva; + std::string Name; + std::string Library; +}; + struct MacOsImageInput { bool Executable = true; std::string InstallName; @@ -61,6 +67,8 @@ struct MacOsImageInput { std::vector TlsImports; std::vector TlsExports; const std::vector* Trampolines = nullptr; + bool TraceImports = false; + std::vector Traced; std::function Message; }; diff --git a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsMachOPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsMachOPatcher.hpp index 2f2ffbc485..78b0e33997 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsMachOPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsMachOPatcher.hpp @@ -11,7 +11,7 @@ namespace Elfpatcher::MacOs { class MacOsMachOPatcher final : public IElfPatcher { public: - explicit MacOsMachOPatcher(std::optional fixedBase = std::nullopt, std::function message = {}); + explicit MacOsMachOPatcher(std::optional fixedBase = std::nullopt, std::function message = {}, bool traceImports = false); std::vector Patch( std::vector sourceElf, @@ -27,6 +27,7 @@ public: private: std::optional fixedBase; std::function message; + bool traceImports; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp index 790dfdf3ae..c9436e4915 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp @@ -13,6 +13,7 @@ inline constexpr std::uint32_t SectionAlignment = 0x1000; inline constexpr std::uint32_t FileAlignment = 0x200; inline constexpr std::uint32_t LoadRva = 0x10000; inline constexpr std::uint64_t ImageBase = 0x140000000; +inline constexpr std::uint64_t ExecutableBase = 0x400000; inline constexpr std::uint32_t SectionRead = 0x40000000; inline constexpr std::uint32_t SectionWrite = 0x80000000; inline constexpr std::uint32_t SectionExecute = 0x20000000; diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp index db44e19058..46e9e66fed 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp @@ -2,13 +2,12 @@ #define ELFPATCHER_WINDOWS_PEWRITER_HPP #include -#include namespace Elfpatcher::Windows { class WindowsPeWriter { public: - std::vector Write(const std::vector& sections, std::uint32_t entryRva, const std::array& directories, bool windowsGui = false, std::optional fixedImageBase = std::nullopt, bool dynamicBase = true) const; + std::vector Write(const std::vector& sections, std::uint32_t entryRva, const std::array& directories, bool windowsGui = false, std::uint64_t imageBase = ImageBase, bool fixedBase = false, bool dynamicBase = true) const; }; } diff --git a/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp b/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp index e07068514f..ec6ea6cb35 100644 --- a/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp +++ b/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp @@ -103,6 +103,7 @@ void SectionHeaderTableBuilder::WriteTable( for (const char i : kShStrTabBlob) buf.push_back(static_cast(i)); + Io::AlignBuffer(buf, std::size_t{8}); const auto shOff = static_cast(buf.size()); buf.resize(buf.size() + kShdrEntrySize * kSectionHeaderCount, 0); diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index b2f84ae52d..c2e89787cc 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -22,6 +22,8 @@ namespace Elfpatcher::Linux { namespace { constexpr std::uint64_t kLinuxUserAddressEnd = 0x800000000000; +constexpr std::uint64_t kLazyPltGotBytes = 24; +constexpr std::uint64_t kPltGotEntryBytes = 8; std::string Hex(const std::uint64_t value) { std::ostringstream text; @@ -81,7 +83,6 @@ std::vector RebasedRelocations(const std::vector& ta } case R_X86_64_64: case R_X86_64_PC32: - case R_X86_64_COPY: case R_X86_64_GLOB_DAT: case R_X86_64_JUMP_SLOT: case R_X86_64_32: @@ -102,6 +103,19 @@ void RequireFileRange(const std::vector& buf, const Domain::Progra throw Domain::RelinkerException("PT_LOAD at " + Hex(load.MappedAddress) + " has file bytes outside the input"); } +void RequireLoaded(const std::vector& headers, const std::uint64_t address, const std::uint64_t size, const std::uint32_t flags, const std::string& what) { + for (const auto& header : headers) + if (header.Type == PT_LOAD && (header.Flags == 0 || (header.Flags & flags) == flags) && address >= header.MappedAddress && address - header.MappedAddress < header.MemorySize && size <= header.MemorySize - (address - header.MappedAddress)) + return; + const auto range = size > 1 ? " (" + std::to_string(size) + " bytes) does not fit inside " : std::string(" is not inside "); + throw Domain::RelinkerException(what + " " + Hex(address) + range + ((flags & PF_X) != 0 ? "an executable" : "a") + " PT_LOAD segment"); +} + +std::uint64_t RelocationWidth(const std::vector& relocations, const std::size_t entry) { + const auto type = static_cast(Io::ReadU64(relocations, entry + 8)); + return type == R_X86_64_PC32 || type == R_X86_64_32 ? 4 : 8; +} + void SeparateSharedLoadPages(std::vector& buf, std::vector& headers) { std::vector loads; for (std::size_t index = 0; index < headers.size(); ++index) { @@ -113,6 +127,11 @@ void SeparateSharedLoadPages(std::vector& buf, std::vector LinuxElfPatcher::Patch( const std::vector& trampolines) { const auto dynSection = ImportsByNid(sourceDynSection); + if (runPath.empty() || runPath.front() == ':' || runPath.back() == ':' || runPath.find("::") != std::string::npos) + throw Domain::RelinkerException("Empty Linux run path component is not supported"); if (dependencyDiagnostics) throw Domain::RelinkerException("Linux target does not support --windows-diagnostics"); if (_fixedBase && lazyBinding) @@ -357,6 +378,12 @@ std::vector LinuxElfPatcher::Patch( throw Domain::RelinkerException("--trace-imports requires libc.prx among the libraries"); if (_fixedBase) RequireOnlyDependencies(dynSection.DynamicSegmentData); + RequireLoaded(originalHeaders, Io::ReadU64(sourceElf, kEhdrEntryOffset), 1, PF_X, "e_entry"); + for (const auto& [relocationTable, name] : {std::pair{&dynSection.RelaData, "RELA r_offset"}, std::pair{&dynSection.RelaPltData, "JMPREL r_offset"}}) + for (std::size_t entry = 0; entry < relocationTable->size(); entry += kRelaEntrySize) + RequireLoaded(originalHeaders, Io::ReadU64(*relocationTable, entry), RelocationWidth(*relocationTable, entry), 0, name); + if (!dynSection.RelaPltData.empty()) + RequireLoaded(originalHeaders, originalPltGotVaddr, lazyBinding ? kLazyPltGotBytes : kPltGotEntryBytes, 0, "DT_PLTGOT"); const auto traced = _traceImports ? TracedImports(dynSection) : std::vector{}; const std::uint64_t base = _fixedBase.value_or(0); const auto address = [this, base](const std::uint64_t vaddr) { diff --git a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp index b864c5640e..e0e2648f1f 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -95,6 +96,40 @@ constexpr std::uint8_t EntryStub[] = { constexpr std::size_t EntryStubEntryDisplacement = 0x69; constexpr std::size_t EntryStubStartDisplacement = 0x84; +struct MacOsTracePrologue { + std::vector Bytes; + std::size_t EntryDisplacement; +}; + +MacOsTracePrologue BuildTracePrologue(const std::uint32_t prologueRva, const std::uint32_t tableRva, const std::uint32_t tableEndRva, const std::uint32_t functionSlotRva) { + std::vector code = {0x57, 0x56, 0x52, 0x51, 0x53, 0x41, 0x54, 0x48, 0x83, 0xec, 0x08}; + const auto rip = [&](const std::initializer_list opcode, const std::uint32_t target) { + code.insert(code.end(), opcode); + const auto displacement = static_cast(target) - static_cast(prologueRva + code.size() + 4); + if (displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) + throw Domain::RelinkerException("Import trace stub exceeds rel32 range"); + Io::AppendU32(code, static_cast(displacement)); + }; + rip({0x48, 0x8d, 0x1d}, tableRva); + rip({0x4c, 0x8d, 0x25}, tableEndRva); + 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}, functionSlotRva); + code.insert(code.end(), {0x48, 0x8b, 0x0b, 0x48, 0x89, 0x04, 0x0b, 0x48, 0x83, 0xc3, 0x18, 0xeb}); + const auto back = static_cast(loop) - static_cast(code.size() + 1); + if (back < -128 || back > 127) + throw Domain::RelinkerException("Import trace loop exceeds rel8 range"); + code.push_back(static_cast(static_cast(back))); + if (code.size() - exit > 127) + throw Domain::RelinkerException("Import trace loop exceeds rel8 range"); + code[exit - 1] = static_cast(code.size() - exit); + code.insert(code.end(), {0x48, 0x83, 0xc4, 0x08, 0x41, 0x5c, 0x5b, 0x59, 0x5a, 0x5e, 0x5f, 0xe9, 0x00, 0x00, 0x00, 0x00}); + const auto entry = code.size() - 4; + return {std::move(code), entry}; +} + struct Section { std::string Name; std::uint64_t Offset; @@ -491,7 +526,8 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { const auto descriptor = metaRva + static_cast(MetaTlsDescriptor); const auto initPointer = AlignUp(metaSize, 8); const auto startSlot = initPointer + 8; - const auto tlsExports = startSlot + 8; + const auto traceFunctionSlot = input.TraceImports ? startSlot + 8 : startSlot; + const auto tlsExports = traceFunctionSlot + 8; std::map tlsImportSlots; for (const auto& import : input.TlsImports) tlsImportSlots.emplace(import.Symbol, 0); auto metaEnd = tlsExports + input.TlsExports.size() * 16; @@ -666,6 +702,44 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { } if (input.Executable) { input.Binds.push_back({"_Aps5StartGuest_nid_no_patch", metaRva + static_cast(startSlot)}); + if (input.TraceImports) + input.Binds.push_back({"_TraceImport_nid_no_patch", metaRva + static_cast(traceFunctionSlot)}); + } + std::size_t traceEntryPatch = 0; + std::uint32_t tracePrologueRva = 0; + if (input.Executable && input.TraceImports) { + const auto tableRva = code.GetRva(); + const auto tableEnd = tableRva + static_cast(input.Traced.size() * 24); + std::map stringRva; + std::vector stringOrder; + auto cursor = tableEnd; + const auto intern = [&](const std::string& text) { + const auto existing = stringRva.find(text); + if (existing != stringRva.end()) return existing->second; + stringRva.emplace(text, cursor); + stringOrder.push_back(text); + cursor += static_cast(text.size() + 1); + return stringRva.at(text); + }; + std::vector table; + for (std::size_t index = 0; index < input.Traced.size(); ++index) { + const auto entry = tableRva + static_cast(index * 24); + const auto relative = [entry](const std::uint32_t target) { return static_cast(static_cast(target) - static_cast(entry)); }; + Io::AppendU64(table, relative(input.Traced[index].Rva)); + Io::AppendU64(table, relative(intern(input.Traced[index].Name))); + Io::AppendU64(table, relative(intern(input.Traced[index].Library))); + } + for (const auto byte : table) code.Emit({byte}); + for (const auto& text : stringOrder) { + if (code.GetRva() != stringRva.at(text)) throw Domain::RelinkerException("Import trace string was placed at the wrong address"); + for (const auto character : text) code.Emit({static_cast(character)}); + code.Emit({0}); + } + if (code.GetRva() != cursor) throw Domain::RelinkerException("Import trace table size changed while it was written"); + tracePrologueRva = cursor; + const auto prologue = BuildTracePrologue(cursor, tableRva, tableEnd, metaRva + static_cast(traceFunctionSlot)); + traceEntryPatch = static_cast(code.GetRva() - codeRva) + prologue.EntryDisplacement; + for (const auto byte : prologue.Bytes) code.Emit({byte}); } const auto metaBytes = meta.size(); segments.push_back({"__APS5DATA", metaRva, metaBytes, VmProtRead | VmProtWrite, std::move(meta), std::move(dataSections)}); @@ -680,6 +754,8 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { while (used != 0 && data[used - 1] == 0) --used; data.resize(used); } + if (segments[index].Name == "__APS5TEXT" && traceEntryPatch != 0 && data.size() < traceEntryPatch + 4) + data.resize(traceEntryPatch + 4, 0); data.resize(AlignUp(data.size(), PageSize), 0); } @@ -817,16 +893,32 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { std::vector file; constexpr std::size_t HeaderSize = 32; const auto stubOffset = AlignUp(HeaderSize + commands.size(), 16); - if (stubOffset + (input.Executable ? sizeof(EntryStub) : 0) > LoadRva) + const auto traceJump = input.Executable && input.TraceImports; + const auto entryOffset = stubOffset + (traceJump ? 5 : 0); + if (entryOffset + (input.Executable ? sizeof(EntryStub) : 0) > LoadRva) throw Domain::RelinkerException("Mach-O load commands do not fit before the guest image"); file.resize(LoadRva, 0); if (input.Executable) { - std::copy(std::begin(EntryStub), std::end(EntryStub), file.begin() + static_cast(stubOffset)); - const auto writeDisplacement = [&](const std::size_t field, const std::uint64_t targetRva) { - const auto displacement = static_cast(targetRva) - static_cast(stubOffset + field + 4); + if (traceJump) { + const auto text = std::find_if(segments.begin(), segments.end(), [](const Segment& segment) { return segment.Name == "__APS5TEXT"; }); + if (text == segments.end() || traceEntryPatch + 4 > text->Data.size()) + throw Domain::RelinkerException("Import trace prologue is missing from the Mach-O image"); + const auto displacement = static_cast(entryOffset) - static_cast(codeRva + traceEntryPatch + 4); if (displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) throw Domain::RelinkerException("macOS entry stub target is out of rel32 range"); - Io::WriteU32(file, stubOffset + field, static_cast(static_cast(displacement))); + Io::WriteU32(text->Data, traceEntryPatch, static_cast(static_cast(displacement))); + const auto jump = static_cast(tracePrologueRva) - static_cast(stubOffset + 5); + if (jump < std::numeric_limits::min() || jump > std::numeric_limits::max()) + throw Domain::RelinkerException("Import trace prologue is out of rel32 range"); + file[stubOffset] = 0xe9; + Io::WriteU32(file, stubOffset + 1, static_cast(static_cast(jump))); + } + std::copy(std::begin(EntryStub), std::end(EntryStub), file.begin() + static_cast(entryOffset)); + const auto writeDisplacement = [&](const std::size_t field, const std::uint64_t targetRva) { + const auto displacement = static_cast(targetRva) - static_cast(entryOffset + field + 4); + if (displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) + throw Domain::RelinkerException("macOS entry stub target is out of rel32 range"); + Io::WriteU32(file, entryOffset + field, static_cast(static_cast(displacement))); }; writeDisplacement(EntryStubEntryDisplacement, input.EntryRva); writeDisplacement(EntryStubStartDisplacement, metaRva + startSlot); diff --git a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp index ff62ba6e2d..3cb70284e7 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp @@ -51,7 +51,7 @@ std::string MachODependency(const std::string& name, const std::string& origin) return "@rpath/" + name; } -MacOsMachOPatcher::MacOsMachOPatcher(const std::optional fixedBase, std::function message) : fixedBase(fixedBase), message(std::move(message)) { +MacOsMachOPatcher::MacOsMachOPatcher(const std::optional fixedBase, std::function message, const bool traceImports) : fixedBase(fixedBase), message(std::move(message)), traceImports(traceImports) { if (fixedBase && (*fixedBase < 0x20000 || *fixedBase > 0x7fffffff0000 || (*fixedBase & 0xffff) != 0)) throw Domain::RelinkerException("Invalid fixed macOS guest image base"); } @@ -65,6 +65,9 @@ std::vector MacOsMachOPatcher::Patch(std::vector sou if (originalPltGotVaddr != 0) image.GetRva(originalPltGotVaddr, 8); const auto relocations = Windows::WindowsRelocationBuilder().Apply(image, dynamicSection, originalHeaders); + const auto needed = ReadNeededLibraries(dynamicSection); + if (traceImports && std::find(needed.begin(), needed.end(), "libc.prx") == needed.end()) + throw Domain::RelinkerException("--trace-imports requires libc.prx among the libraries"); MacOsImageInput input; input.Executable = true; @@ -98,9 +101,13 @@ std::vector MacOsMachOPatcher::Patch(std::vector sou } } input.Rebases = relocations.BaseRelocations; - for (const auto& import : relocations.Imports) + for (const auto& import : relocations.Imports) { input.Binds.push_back({"_" + import.Name, import.TargetRva, static_cast(import.Addend), import.Weak}); - for (const auto& library : ReadNeededLibraries(dynamicSection)) + if (traceImports && import.Function && !import.Weak && import.Addend == 0 && !import.Name.empty()) + input.Traced.push_back({import.TargetRva, import.Name, import.Library}); + } + input.TraceImports = traceImports; + for (const auto& library : needed) input.Dylibs.push_back(MachODependency(library, "@executable_path")); for (const auto* library : {"@rpath/libkernel.prx", "@rpath/libc.prx"}) if (std::find(input.Dylibs.begin(), input.Dylibs.end(), library) == input.Dylibs.end()) diff --git a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp index 2be086cc21..ec473d8028 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -114,6 +114,15 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con const auto logName = addWideString(logFileName); const auto shellLibrary = addString("shell32.dll"); const auto parseArguments = addString("CommandLineToArgvW"); + const auto runtimeModule = reserve(8); + const auto runtimeIobFunction = reserve(8); + const auto runtimeFreopenFunction = reserve(8); + const auto msvcrtLibrary = addWideString(u"msvcrt.dll"); + const auto ucrtbaseLibrary = addWideString(u"ucrtbase.dll"); + const auto msvcrtIobFunctionName = addString("__iob_func"); + const auto ucrtbaseIobFunctionName = addString("__acrt_iob_func"); + const auto runtimeFreopenName = addString("_wfreopen"); + const auto runtimeAppendMode = addWideString(u"a"); const auto handles = reserve(libraries.size() * 8); const auto guestFinished = reserve(4); const WindowsGuestStartup guestStartup; @@ -413,7 +422,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con code.U32(units); code.Emit({0x66, 0xf3, 0xa5}); code.Rip({0x48, 0x8d, 0x0d}, modulePath); - code.Emit({0xba, 0, 0, 0, 0x40, 0x41, 0xb8, 3, 0, 0, 0, 0x45, 0x31, 0xc9}); + code.Emit({0xba, 4, 0, 0, 0, 0x41, 0xb8, 3, 0, 0, 0, 0x45, 0x31, 0xc9}); code.Emit({0x48, 0xc7, 0x44, 0x24, 0x20, 2, 0, 0, 0, 0x48, 0xc7, 0x44, 0x24, 0x28, 0x80, 0, 0, 0, 0x48, 0xc7, 0x44, 0x24, 0x30, 0, 0, 0, 0}); call("CreateFileW"); code.Emit({0x66, 0x41, 0xc7, 0x04, 0x24, 0x00, 0x00}); @@ -431,6 +440,51 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con code.PatchBranch(failed, done); }; + const auto rebindPreloadedRuntimeLog = [&](const std::uint32_t runtimeLibrary, const std::uint32_t iobFunctionName, const bool indexedIob) { + code.Rip({0x48, 0x8d, 0x0d}, runtimeLibrary); + call("GetModuleHandleW"); + code.Emit({0x48, 0x85, 0xc0}); + std::vector absent = {code.Branch({0x0f, 0x84})}; + code.Rip({0x48, 0x89, 0x05}, runtimeModule); + code.Emit({0x48, 0x89, 0xc1}); + code.Rip({0x48, 0x8d, 0x15}, iobFunctionName); + call("GetProcAddress"); + code.Emit({0x48, 0x85, 0xc0}); + absent.push_back(code.Branch({0x0f, 0x84})); + code.Rip({0x48, 0x89, 0x05}, runtimeIobFunction); + code.Rip({0x48, 0x8b, 0x0d}, runtimeModule); + code.Rip({0x48, 0x8d, 0x15}, runtimeFreopenName); + call("GetProcAddress"); + code.Emit({0x48, 0x85, 0xc0}); + absent.push_back(code.Branch({0x0f, 0x84})); + code.Rip({0x48, 0x89, 0x05}, runtimeFreopenFunction); + for (const auto& stream : {std::pair{0xfffffff5u, 1}, {0xfffffff4u, 2}}) { + code.Emit({0xb9}); + code.U32(stream.first); + call("GetStdHandle"); + code.Emit({0x48, 0x85, 0xc0}); + const auto missing = code.Branch({0x0f, 0x84}); + code.Rip({0x48, 0x3b, 0x05}, logHandle); + const auto notRedirected = code.Branch({0x0f, 0x85}); + if (indexedIob) { + code.Emit({0xb9, stream.second, 0, 0, 0}); + code.Rip({0xff, 0x15}, runtimeIobFunction); + code.Emit({0x49, 0x89, 0xc0}); + } else { + code.Rip({0xff, 0x15}, runtimeIobFunction); + code.Emit({0x4c, 0x8d, 0x40, static_cast(48 * stream.second)}); + } + code.Rip({0x48, 0x8d, 0x0d}, logName); + code.Rip({0x48, 0x8d, 0x15}, runtimeAppendMode); + code.Rip({0xff, 0x15}, runtimeFreopenFunction); + const auto next = code.GetRva(); + code.PatchBranch(missing, next); + code.PatchBranch(notRedirected, next); + } + const auto done = code.GetRva(); + for (const auto branch : absent) code.PatchBranch(branch, done); + }; + code.Emit({0x53, 0x55, 0x56, 0x57, 0x41, 0x54, 0x48, 0x83, 0xec, 0x60}); code.Rip({0x48, 0x8d, 0x0d}, parentVariable); code.Rip({0x48, 0x8d, 0x15}, parentText); @@ -505,6 +559,8 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con code.Rip({0x48, 0x8d, 0x0d}, modulePath); call("SetCurrentDirectoryW"); requireNonzero(directoryError, 0xc000000du); + rebindPreloadedRuntimeLog(msvcrtLibrary, msvcrtIobFunctionName, false); + rebindPreloadedRuntimeLog(ucrtbaseLibrary, ucrtbaseIobFunctionName, true); if (dependencyDiagnostics) { code.Rip({0x4c, 0x8d, 0x05}, programPath); writeUtf8(programText); @@ -817,6 +873,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con for (const auto index : lazyUnresolvedImports) { const auto stubRva = code.GetRva(); + code.Emit({0x48, 0x83, 0xe4, 0xf0, 0x48, 0x83, 0xec, 0x40}); writeString(errorRvas.at(3 + index), true); raise(0xc0000139u); result.LazyStubs.push_back({imports[index].TargetRva, stubRva}); diff --git a/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp b/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp index 9f100e655c..c181efa7d2 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp @@ -7,10 +7,10 @@ namespace Elfpatcher::Windows { WindowsLoadImage::WindowsLoadImage(const std::vector& source, const std::vector& headers, const bool requireEntry, const std::uint64_t imageBase) : imageBase(imageBase) { if (source.size() < 64 || Io::ReadU32(source, 0) != 0x464c457f || source[4] != 2 || source[5] != 1 || source[6] != 1 || Io::ReadU16(source, 18) != 62) - throw Domain::RelinkerException("Windows output requires a little-endian ELF64 x86-64 image"); + throw Domain::RelinkerException("This target requires a little-endian ELF64 x86-64 image"); const auto type = Io::ReadU16(source, 16); if (type != 3 && type != 0xfe10 && type != 0xfe18) - throw Domain::RelinkerException("Windows output requires a position-independent ELF image"); + throw Domain::RelinkerException("This target requires a position-independent ELF image"); for (const auto& header : headers) { if (header.Type != 1) continue; diff --git a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp index 063af7924b..36e88bf315 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp @@ -39,7 +39,7 @@ std::vector WindowsPePatcher::Patch(std::vector sour const auto report = [&](std::uint64_t completed) { if (progress) progress(completed, 10000); }; report(0); diagnostics.clear(); - WindowsLoadImage image(sourceElf, originalHeaders, true, fixedBase ? *fixedBase - LoadRva : ImageBase); + WindowsLoadImage image(sourceElf, originalHeaders, true, fixedBase.value_or(ExecutableBase) - LoadRva); if (originalPltGotVaddr != 0) image.GetRva(originalPltGotVaddr, 8); report(500); @@ -111,7 +111,7 @@ std::vector WindowsPePatcher::Patch(std::vector sour } directories[2] = icon ? WindowsIconResourceBuilder().Build(*icon, iconName, sections, nextRva) : PeDirectory{}; report(9500); - auto result = WindowsPeWriter().Write(sections, entryRva, directories, windowsGui, fixedBase ? std::optional(image.GetImageBase()) : std::nullopt, false); + auto result = WindowsPeWriter().Write(sections, entryRva, directories, windowsGui, image.GetImageBase(), fixedBase.has_value(), false); report(10000); return result; } diff --git a/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp index 48d867f0f5..123bc83ef7 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp @@ -5,7 +5,7 @@ namespace Elfpatcher::Windows { -std::vector WindowsPeWriter::Write(const std::vector& sections, const std::uint32_t entryRva, const std::array& directories, const bool windowsGui, const std::optional fixedImageBase, const bool dynamicBase) const { +std::vector WindowsPeWriter::Write(const std::vector& sections, const std::uint32_t entryRva, const std::array& directories, const bool windowsGui, const std::uint64_t imageBase, const bool fixedBase, const bool dynamicBase) const { constexpr std::size_t peOffset = 0x80; constexpr std::size_t optionalOffset = peOffset + 24; constexpr std::size_t sectionTable = optionalOffset + 240; @@ -18,12 +18,12 @@ std::vector WindowsPeWriter::Write(const std::vector& s Io::WriteU16(result, peOffset + 4, 0x8664); Io::WriteU16(result, peOffset + 6, static_cast(sections.size())); Io::WriteU16(result, peOffset + 20, 240); - if (fixedImageBase && (directories[5].Size != 0 || *fixedImageBase < 0x10000 || (*fixedImageBase & 0xffff) != 0)) - throw Domain::RelinkerException("Invalid fixed Windows image base or base relocations"); - Io::WriteU16(result, peOffset + 22, fixedImageBase ? 0x23 : 0x22); + if (imageBase < 0x10000 || (imageBase & 0xffff) != 0 || (fixedBase && directories[5].Size != 0)) + throw Domain::RelinkerException("Invalid Windows image base or base relocations"); + Io::WriteU16(result, peOffset + 22, fixedBase ? 0x23 : 0x22); Io::WriteU16(result, optionalOffset, 0x20b); Io::WriteU32(result, optionalOffset + 16, entryRva); - Io::WriteU64(result, optionalOffset + 24, fixedImageBase.value_or(ImageBase)); + Io::WriteU64(result, optionalOffset + 24, imageBase); Io::WriteU32(result, optionalOffset + 32, SectionAlignment); Io::WriteU32(result, optionalOffset + 36, FileAlignment); Io::WriteU16(result, optionalOffset + 40, 6); @@ -72,7 +72,7 @@ std::vector WindowsPeWriter::Write(const std::vector& s } if (!foundEntry) throw Domain::RelinkerException("PE entry point is not inside executable section data", entryRva); - if (fixedImageBase && *fixedImageBase > 0x7fffffffffff - static_cast(endRva)) + if (fixedBase && imageBase > 0x7fffffff0000 - static_cast(endRva)) throw Domain::RelinkerException("Fixed Windows image exceeds the user address space"); Io::WriteU32(result, optionalOffset + 4, codeSize); Io::WriteU32(result, optionalOffset + 8, dataSize); diff --git a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp index 7bd368ac26..48e601ebf4 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp @@ -25,7 +25,7 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom const auto type = static_cast(info); const auto symbol = info >> 32; if ((plt && type != 7) || (type != 1 && type != 6 && type != 7 && type != 8 && type != 16 && type != 17 && type != 18)) - throw Domain::RelinkerException("Unsupported Windows relocation type " + std::to_string(type), target); + throw Domain::RelinkerException("Relocation type " + std::to_string(type) + " is not supported for this target", target); if (target > std::numeric_limits::max() - 8) throw Domain::RelinkerException("Relocation target address overflow", target); const auto rva = image.GetRva(target, 8); diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index b78a010b40..bd5ce8a18c 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -221,15 +221,21 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co } report(9000); - for (const auto& [rva, length] : gsTraps) + std::sort(branchTargets.begin(), branchTargets.end()); + branchTargets.erase(std::unique(branchTargets.begin(), branchTargets.end()), branchTargets.end()); + for (const auto& [rva, length] : gsTraps) { + const auto target = std::upper_bound(branchTargets.begin(), branchTargets.end(), rva); + if (target != branchTargets.end() && *target < rva + length) + throw Domain::RelinkerException("Branch enters a guest GS instruction", *target); trapInstruction(sections, rva, length); + } if (tls == nullptr) { if (!accesses.empty()) throw Domain::RelinkerException("Guest TLS access without PT_TLS"); return {}; } - if (tls->FileSize > tls->MemorySize || tls->Offset > source.size() || tls->FileSize > source.size() - tls->Offset || (tls->Alignment > 1 && !std::has_single_bit(tls->Alignment)) || tls->Alignment > 8192 || tls->MemorySize > 0x7fff0000u) + if (tls->FileSize > tls->MemorySize || (tls->FileSize != 0 && (tls->Offset > source.size() || tls->FileSize > source.size() - tls->Offset)) || (tls->Alignment > 1 && !std::has_single_bit(tls->Alignment)) || tls->Alignment > 8192 || tls->MemorySize > 0x7fff0000u) throw Domain::RelinkerException("Invalid or unsupported ELF TLS layout", tls->Offset); // Native homebrew linkers can emit an empty PT_TLS placeholder. // It needs no Windows TLS directory unless guest code accesses TLS. @@ -274,9 +280,6 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co code.PatchBranch(skipCallback, code.GetRva()); code.Emit({0xc3}); - std::sort(branchTargets.begin(), branchTargets.end()); - branchTargets.erase(std::unique(branchTargets.begin(), branchTargets.end()), branchTargets.end()); - std::uint64_t patched = 0; for (const auto& access : accesses) { if (patched % 256 == 0) report(9000 + 900 * patched / accesses.size()); diff --git a/core/relinker/elfpatcher/tests/SectionRangeTests.cpp b/core/relinker/elfpatcher/tests/SectionRangeTests.cpp index c75c4e9773..b5ff0514a0 100644 --- a/core/relinker/elfpatcher/tests/SectionRangeTests.cpp +++ b/core/relinker/elfpatcher/tests/SectionRangeTests.cpp @@ -1,6 +1,8 @@ #include +#include #include #include +#include #include #include #include @@ -29,6 +31,30 @@ int main() { auto empty = valid; empty.StubOffset = 384; empty.StubSize = 0; bytes = Fixture(); builder.WriteTable(bytes, empty); Require(Io::ReadU64(bytes, Io::ReadU64(bytes, 40) + 5 * 64 + 16) == 0x1100); + for (std::size_t tail = 0; tail < 8; ++tail) { + bytes = Fixture(); + bytes.resize(512 + tail, 0xa5); + const auto original = bytes; + const auto originalSize = bytes.size(); + builder.WriteTable(bytes, valid); + const auto alignedTable = Io::ReadU64(bytes, 40); + if (alignedTable % 8 != 0) + throw std::runtime_error("ELF64 section header table is not 8-byte aligned"); + const auto stringOffset = Io::ReadU64(bytes, alignedTable + 64 + 24); + const auto stringSize = Io::ReadU64(bytes, alignedTable + 64 + 32); + const auto stringEnd = stringOffset + stringSize; + Require(stringOffset == originalSize && stringSize == sizeof(Elfpatcher::kShStrTabBlob)); + Require(stringEnd <= alignedTable && alignedTable - stringEnd < 8); + Require(bytes[stringOffset] == 0 && bytes[stringEnd - 1] == 0); + Require(std::equal(original.begin() + 64, original.end(), bytes.begin() + 64)); + Require(std::all_of(bytes.begin() + stringEnd, bytes.begin() + alignedTable, + [](const auto byte) { return byte == 0; })); + Require(bytes.size() == alignedTable + Io::ReadU16(bytes, 60) * 64); + Require(Io::ReadU64(bytes, alignedTable + 2 * 64 + 24) == valid.DynStrOffset); + Require(Io::ReadU64(bytes, alignedTable + 3 * 64 + 24) == valid.DynSymOffset); + Require(Io::ReadU64(bytes, alignedTable + 4 * 64 + 24) == valid.DynamicSegmentOffset); + Require(Io::ReadU64(bytes, alignedTable + 5 * 64 + 24) == valid.StubOffset); + } for (int kind = 0; kind < 11; ++kind) { bytes = Fixture(); auto request = valid; switch (kind) { @@ -49,6 +75,6 @@ int main() { try { builder.WriteTable(bytes, request); } catch (const std::runtime_error&) { rejected = true; } Require(rejected && bytes == before); } - std::cout << "13 section range cases passed\n"; + std::cout << "21 section range and alignment cases passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } } diff --git a/core/relinker/elfpatcher/tests/WindowsImportIdentityTests.cpp b/core/relinker/elfpatcher/tests/WindowsImportIdentityTests.cpp index ffe753bfaf..03243cea6b 100644 --- a/core/relinker/elfpatcher/tests/WindowsImportIdentityTests.cpp +++ b/core/relinker/elfpatcher/tests/WindowsImportIdentityTests.cpp @@ -74,7 +74,7 @@ void generateProbe(const Fs::path& file, const std::string& libraries, const std directories[1] = imports.Directory; directories[3] = startup.ExceptionDirectory; directories[12] = imports.AddressTable; - const auto image = WindowsPeWriter().Write({entry, got, imports.Section, startup.Data, startup.Code}, startup.Code.Rva, directories, false, ImageBase, false); + const auto image = WindowsPeWriter().Write({entry, got, imports.Section, startup.Data, startup.Code}, startup.Code.Rva, directories, false, ImageBase, true, false); std::ofstream stream(file, std::ios::binary); stream.write(reinterpret_cast(image.data()), static_cast(image.size())); require(static_cast(stream), "Cannot write " + file.string()); diff --git a/core/relinker/elfpatcher/tests/WindowsMsvcrt.def b/core/relinker/elfpatcher/tests/WindowsMsvcrt.def index c163db8f44..741b17b119 100644 --- a/core/relinker/elfpatcher/tests/WindowsMsvcrt.def +++ b/core/relinker/elfpatcher/tests/WindowsMsvcrt.def @@ -2,3 +2,4 @@ LIBRARY msvcrt.dll EXPORTS __iob_func fputs + fflush diff --git a/core/relinker/elfpatcher/tests/WindowsMsvcrtStdioHost.cpp b/core/relinker/elfpatcher/tests/WindowsMsvcrtStdioHost.cpp index 4c370e0924..2a77b5b41f 100644 --- a/core/relinker/elfpatcher/tests/WindowsMsvcrtStdioHost.cpp +++ b/core/relinker/elfpatcher/tests/WindowsMsvcrtStdioHost.cpp @@ -13,10 +13,13 @@ static_assert(sizeof(_iobuf) == 48); extern "C" _iobuf* __cdecl __iob_func(); extern "C" int __cdecl fputs(const char*, _iobuf*); +extern "C" int __cdecl fflush(_iobuf*); extern "C" int shared() { auto* streams = __iob_func(); fputs("Windows DLL stdout probe\n", streams + 1); fputs("Windows DLL stderr probe\n", streams + 2); + fflush(streams + 1); + fflush(streams + 2); return 11; } diff --git a/core/relinker/elfpatcher/tests/WindowsStdioHost.cpp b/core/relinker/elfpatcher/tests/WindowsStdioHost.cpp index 72a29373f4..3d996c8df4 100644 --- a/core/relinker/elfpatcher/tests/WindowsStdioHost.cpp +++ b/core/relinker/elfpatcher/tests/WindowsStdioHost.cpp @@ -3,5 +3,7 @@ extern "C" int shared() { std::fputs("Windows DLL stdout probe\n", stdout); std::fputs("Windows DLL stderr probe\n", stderr); + std::fflush(stdout); + std::fflush(stderr); return 11; } diff --git a/core/relinker/io/src/FileWriter.cpp b/core/relinker/io/src/FileWriter.cpp index 49df133411..10606d09bb 100644 --- a/core/relinker/io/src/FileWriter.cpp +++ b/core/relinker/io/src/FileWriter.cpp @@ -33,11 +33,10 @@ int createTemporary(const std::filesystem::path& target, const mode_t mode, std: constexpr std::string_view characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; std::random_device device; for (int attempt = 0; attempt < 100; ++attempt) { - std::string suffix = "."; + std::string suffix = ".anyps5-"; for (int index = 0; index < 6; ++index) suffix += characters[device() % characters.size()]; - temporary = target; - temporary += suffix; + temporary = target.parent_path() / suffix; const int descriptor = open(temporary.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, mode); if (descriptor >= 0 || errno != EEXIST) return descriptor; diff --git a/core/relinker/io/src/NativePath.cpp b/core/relinker/io/src/NativePath.cpp index 13b11fa505..ad603ef310 100644 --- a/core/relinker/io/src/NativePath.cpp +++ b/core/relinker/io/src/NativePath.cpp @@ -1,8 +1,11 @@ #include +#include namespace Io { std::filesystem::path NativePath(const std::string& path) { + if (path.find('\0') != std::string::npos) + throw std::invalid_argument("File path contains a NUL byte"); const std::u8string utf8(path.begin(), path.end()); return std::filesystem::path(utf8); } diff --git a/core/relinker/io/tests/FileWriterTests.cpp b/core/relinker/io/tests/FileWriterTests.cpp index 8ec3b79abd..7ab1724d30 100644 --- a/core/relinker/io/tests/FileWriterTests.cpp +++ b/core/relinker/io/tests/FileWriterTests.cpp @@ -40,15 +40,15 @@ std::vector siblings(const std::filesystem::path& output) { std::vector names; for (const auto& entry : std::filesystem::directory_iterator(output.parent_path())) { const auto name = entry.path().filename().string(); - if (name.starts_with(output.filename().string() + ".")) + if (name.starts_with(output.filename().string() + ".") || name.starts_with(".anyps5-")) names.push_back(name); } return names; } template -void requireRunningReplacement(const std::filesystem::path& directory, const TValue& data, const bool leftover) { - const auto output = directory / (leftover ? "leftover" : "running"); +void requireRunningReplacement(const std::filesystem::path& directory, const TValue& data, const bool leftover, const std::string& name = "running") { + const auto output = directory / (leftover ? "leftover" : name); if (leftover) std::ofstream(directory / "leftover.tmp", std::ios::binary) << "killed"; std::filesystem::copy_file(std::filesystem::read_symlink("/proc/self/exe"), output); @@ -107,8 +107,8 @@ void requireRunningReplacement(const std::filesystem::path& directory, const TVa } template -void requireFailedReplacement(const std::filesystem::path& directory, const TValue& data) { - const auto output = directory / "limited"; +void requireFailedReplacement(const std::filesystem::path& directory, const TValue& data, const std::string& name = "limited") { + const auto output = directory / name; std::ofstream(output, std::ios::binary) << "previous"; rlimit limit{}; getrlimit(RLIMIT_FSIZE, &limit); @@ -158,6 +158,11 @@ void requireReplacement(const TValue& data) { requireRunningReplacement(directory, data, false); requireRunningReplacement(directory, data, true); requireFailedReplacement(directory, data); + const auto maximumName = pathconf(directory.c_str(), _PC_NAME_MAX); + if (maximumName <= 0) + throw std::runtime_error("Cannot determine the output filename limit"); + requireFailedReplacement(directory, data, std::string(static_cast(maximumName), 'f')); + requireRunningReplacement(directory, data, false, std::string(static_cast(maximumName), 'r')); requireInPlaceWrites(directory, data); } catch (...) { std::filesystem::remove_all(directory, error); diff --git a/core/relinker/io/tests/NativePathTests.cpp b/core/relinker/io/tests/NativePathTests.cpp new file mode 100644 index 0000000000..e40cd5f3cf --- /dev/null +++ b/core/relinker/io/tests/NativePathTests.cpp @@ -0,0 +1,74 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +template +void RequireInvalidPath(TAction action) { + try { + action(); + } catch (const std::invalid_argument& error) { + Require(std::string(error.what()).find("NUL") != std::string::npos, "Path error does not identify the NUL byte"); + return; + } + throw std::runtime_error("A path containing a NUL byte was accepted"); +} + +void Conversions() { + for (const std::string path : {"", "relative/file.bin", "a path with spaces", "caf\xc3\xa9/\xe6\x97\xa5\xe6\x9c\xac.bin"}) + Require(Io::Utf8Path(Io::NativePath(path)) == path, "Valid path did not round-trip"); + for (const std::string prefix : {"", "file", "directory/file"}) { + for (const auto& suffix : std::vector{"", "suffix", std::string(1, '\0')}) { + const auto path = prefix + '\0' + suffix; + RequireInvalidPath([&] { Io::NativePath(path); }); + } + } +} + +void FileAccess(const std::filesystem::path& directory) { + const auto original = directory / Io::NativePath("caf\xc3\xa9-\xe6\x97\xa5\xe6\x9c\xac.bin"); + const auto originalPath = Io::Utf8Path(original); + const std::vector expected{0, 1, 10, 13, 127, 128, 255}; + Io::FileWriter().Write(originalPath, expected); + Require(Io::FileReader().Read(originalPath) == expected, "Valid UTF-8 file path did not preserve binary data"); + const auto missingPath = Io::Utf8Path(directory / "missing.bin"); + for (const auto& prefix : {originalPath, missingPath}) { + for (const std::string suffix : {"", ".other", "/other.bin"}) { + const auto path = prefix + '\0' + suffix; + RequireInvalidPath([&] { Io::FileReader().Read(path); }); + RequireInvalidPath([&] { Io::FileWriter().Write(path, std::vector{42}); }); + RequireInvalidPath([&] { Io::FileWriter().Write(path, std::string("replacement")); }); + Require(Io::FileReader().Read(originalPath) == expected, "Invalid path changed the file named before the NUL byte"); + Require(!std::filesystem::exists(Io::NativePath(missingPath)), "Invalid path created the file named before the NUL byte"); + } + } +} + +} + +int main() { + const auto directory = std::filesystem::temp_directory_path() / + ("anyps5-native-path-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + try { + Conversions(); + std::filesystem::create_directory(directory); + FileAccess(directory); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + std::error_code ignored; + std::filesystem::remove_all(directory, ignored); + return 1; + } + std::filesystem::remove_all(directory); +} diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 6f2a15fe2e..81b6e3d359 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -45,11 +45,12 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, input.Executable = fileReader.Read(inputPath); input.InputPath = std::filesystem::absolute(Io::NativePath(inputPath)).lexically_normal(); input.OutputPath = Io::NativePath(absPath); + Cli::NativeModules modules; if (!args.skipSceModule) { - auto modules = Cli::ReadModules(std::filesystem::absolute(Io::NativePath(args.inputPath)).lexically_normal(), Io::NativePath(args.sceModulePath), args.excludedSceModules, args.moduleDirectories); - input.ModuleRoot = std::move(modules.Root); + modules = Cli::ReadModules(std::filesystem::absolute(Io::NativePath(args.inputPath)).lexically_normal(), Io::NativePath(args.sceModulePath), args.excludedSceModules, args.moduleDirectories); + input.ModuleRoot = modules.Root; input.Modules = std::move(modules.Modules); - input.RuntimeDirectories = std::move(modules.RuntimeDirectories); + input.RuntimeDirectories = modules.RuntimeDirectories; } const auto iconPath = Io::NativePath(args.inputPath).parent_path() / "sce_sys" / "icon0.png"; if (args.toWindows && std::filesystem::exists(iconPath)) input.Icon = fileReader.Read(Io::Utf8Path(iconPath)); @@ -76,6 +77,7 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, progress.IncludeFile(Io::Utf8Path(input.OutputPath)); for (const auto& module : input.Modules) progress.IncludeFile(Io::Utf8Path(module.Path)); auto converted = Relinker::Conversion(progress).Convert(std::move(input), options); + if (!args.skipSceModule) Cli::ReportUnconvertedNeeded(modules, converted.Analysis.DynamicSection.NeededLibraries, args.toWindows, std::cout); return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics), std::move(converted.AppliedPatches), converted.LinuxBase}; } @@ -123,9 +125,10 @@ bool SameOutputFile(const std::filesystem::path& first, const std::filesystem::p return Io::SameFile(first, second); } -bool SameOutputDestination(const std::filesystem::path& first, const std::filesystem::path& second) { - const auto firstPath = std::filesystem::weakly_canonical(first); - const auto secondPath = std::filesystem::weakly_canonical(second); +bool SameOutputDestination(const std::filesystem::path& first, const std::filesystem::path& second, + std::filesystem::path& firstPath, std::filesystem::path& secondPath) { + if (firstPath.empty()) firstPath = std::filesystem::weakly_canonical(first); + if (secondPath.empty()) secondPath = std::filesystem::weakly_canonical(second); if (firstPath == secondPath) return true; if (std::filesystem::exists(first) && std::filesystem::exists(second)) return SameOutputFile(first, second); #ifdef _WIN32 @@ -402,11 +405,12 @@ int Run(const int argc, char* argv[]) { outputPaths.push_back(desktopEntry->EntryPath); } if (args.windowsDiagnostics) outputPaths.push_back(std::filesystem::absolute("windows-diagnostics-imports.txt")); + std::vector canonicalOutputPaths(outputPaths.size()); for (std::size_t index = 0; index < outputPaths.size(); ++index) { for (std::size_t previous = 0; previous < index; ++previous) { const auto& first = outputPaths[index]; const auto& second = outputPaths[previous]; - if (SameOutputDestination(first, second)) + if (SameOutputDestination(first, second, canonicalOutputPaths[index], canonicalOutputPaths[previous])) throw Domain::RelinkerException("Output paths collide: " + Io::Utf8Path(first) + " and " + Io::Utf8Path(second)); } } diff --git a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp index b615250ae8..a99edf14ee 100644 --- a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp +++ b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp @@ -30,6 +30,11 @@ std::optional ripRelativeDispOffset(const std::vector ++cur; continue; } + if (b == PrefixRepne || b == PrefixSegDs) { + hasRexW = false; + ++cur; + continue; + } break; } if (cur + 2 > end) return std::nullopt; diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 2e7d216a7f..6f20ee0a3f 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -71,11 +71,11 @@ std::set CodeInstructionCollector::Collect(const std::ve for (const auto& header : headers) { if (header.Type == 0x61000000) sceBase = header.Offset; if (header.Type != 2) continue; - if (hasDynamic || header.FileSize % 16 != 0) throw Domain::RelinkerException("Code analysis: invalid dynamic segment"); + if (hasDynamic) throw Domain::RelinkerException("Code analysis: invalid dynamic segment"); hasDynamic = true; range(header.Offset, header.FileSize); bool terminated = false; - for (std::uint64_t offset = header.Offset; offset < header.Offset + header.FileSize; offset += 16) { + for (std::uint64_t offset = header.Offset; header.Offset + header.FileSize - offset >= 16; offset += 16) { const auto tag = Io::ReadU64(bytes, offset); if (tag == 0) { terminated = true; break; } const auto value = Io::ReadU64(bytes, offset + 8); @@ -346,7 +346,7 @@ std::set CodeInstructionCollector::Collect(const std::ve addFunction(function.Begin, function.End - function.Begin, false); for (const auto target : function.ExtraTargets) addRoot(target); } - if (sceBase) { + if (sceBase || !functions.empty()) { const std::vector> ranges(functions.begin(), functions.end()); for (const auto& header : codeHeaders) { if (header.Type != 1 || (header.Flags & 1) == 0) continue; diff --git a/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp b/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp index 7c99111960..0171c49c9b 100644 --- a/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp +++ b/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include namespace Relinker { @@ -67,6 +69,183 @@ std::size_t TablesEnd(std::span code, std::size_t firstTable return table; } +bool IsInvalid64BitSingleByteOpcode(std::uint8_t opcode) { + switch (opcode) { + case 0x06: + case 0x07: + case 0x0e: + case 0x16: + case 0x17: + case 0x1e: + case 0x1f: + case 0x27: + case 0x2f: + case 0x37: + case 0x3f: + case 0x60: + case 0x61: + case 0x82: + case 0x9a: + case 0xce: + case 0xd4: + case 0xd5: + case 0xd6: + case 0xea: + return true; + default: + return false; + } +} + +bool IsForbiddenSyscallInstruction(std::span code, std::size_t pos, const Codegen::DecodedInstructionInfo& info) { + const auto opPos = pos + info.OpcodeOffset; + if (opPos + 1 >= pos + info.Length || opPos + 1 >= code.size()) return false; + const auto b0 = code[opPos]; + const auto b1 = code[opPos + 1]; + return (b0 == 0x0f && (b1 == 0x05 || b1 == 0x34 || b1 == 0x07)) || (b0 == 0xcd && b1 == 0x80); +} + +bool IsValid64BitInstruction(std::span code, std::size_t pos, const Codegen::DecodedInstructionInfo& info, VirtualAddress baseVAddr, VirtualAddress segmentVAddr, VirtualAddress segmentEnd) { + if (info.Length == 0 || info.Length > code.size() - pos) return false; + if (!info.IsTwoByteOpcode && IsInvalid64BitSingleByteOpcode(info.Opcode)) return false; + if (info.HasBranchTarget && !info.HasRipRelativeDisp) { + const auto target = static_cast(baseVAddr + pos + info.Length) + info.BranchDisp; + if (target < static_cast(segmentVAddr) || target >= static_cast(segmentEnd)) return false; + } + return true; +} + +bool EndsControlFlow(const Codegen::DecodedInstructionInfo& info, bool isSyscall) { + return info.FlowKind == Codegen::ControlFlowKind::Return + || info.FlowKind == Codegen::ControlFlowKind::UnconditionalJump + || info.FlowKind == Codegen::ControlFlowKind::IndirectJump + || info.FlowKind == Codegen::ControlFlowKind::Trap || isSyscall; +} + +class GapAnalysis { + enum class Trailing { Invalid, Padding, Terminator, NonTerminator }; + +public: + GapAnalysis(std::span code, VirtualAddress baseVAddr, VirtualAddress segmentVAddr, VirtualAddress segmentEnd, const Codegen::X64InstructionDecoder& decoder) + : code(code), padding(code.size() + 1), trailing(code.size() + 1), syscalls(code.size() + 1) { + padding.back() = true; + trailing.back() = Trailing::Padding; + for (std::size_t end = code.size(); end != 0;) { + const auto pos = --end; + const bool bytePadding = code[pos] == 0x00 || code[pos] == 0xcc || code[pos] == 0x90; + if (bytePadding && padding[pos + 1]) { + padding[pos] = true; + trailing[pos] = Trailing::Padding; + continue; + } + Codegen::DecodedInstructionInfo info; + try { + info = decoder.DecodeInstruction(code.data() + pos, code.size() - pos); + } catch (const Codegen::CodegenException&) { + continue; + } + if (info.Length == 0 || info.Length > code.size() - pos) continue; + const bool nop = IsNop(code, pos, info.Length); + if (nop && padding[pos + info.Length]) { + padding[pos] = true; + trailing[pos] = Trailing::Padding; + continue; + } + if (!IsValid64BitInstruction(code, pos, info, baseVAddr, segmentVAddr, segmentEnd)) continue; + const auto next = pos + info.Length; + const bool isSyscall = IsForbiddenSyscallInstruction(code, pos, info); + syscalls[pos] = isSyscall || syscalls[next]; + if (EndsControlFlow(info, isSyscall)) { + trailing[pos] = isSyscall || trailing[next] == Trailing::Padding ? Trailing::Terminator : Trailing::NonTerminator; + continue; + } + if (trailing[next] != Trailing::Padding) { + trailing[pos] = trailing[next]; + continue; + } + trailing[pos] = nop || code[pos] == 0xcc || code[pos] == 0x00 ? Trailing::Padding : Trailing::NonTerminator; + } + } + + bool OnlyPadding(std::size_t from) const { return padding[from]; } + + bool DecodesAsTrailingCodeBlock(std::size_t from, bool* outHasSyscall = nullptr) const { + if (outHasSyscall != nullptr) *outHasSyscall = false; + if (from >= code.size() || code[from] == 0x00 || code[from] == 0xcc || code[from] == 0x90) return false; + if (outHasSyscall != nullptr) *outHasSyscall = syscalls[from]; + return trailing[from] == Trailing::Terminator; + } + +private: + std::span code; + std::vector padding; + std::vector trailing; + std::vector syscalls; +}; + +std::size_t FindEmbeddedDataTableEnd(std::span code, VirtualAddress tableVAddr, VirtualAddress segmentVAddr, VirtualAddress segmentEnd, const Codegen::X64InstructionDecoder& decoder) { + const GapAnalysis analysis(code, tableVAddr, segmentVAddr, segmentEnd, decoder); + std::optional firstInvalidPos; + for (std::size_t pos = 0; pos < code.size();) { + if (analysis.OnlyPadding(pos)) break; + Codegen::DecodedInstructionInfo info; + try { + info = decoder.DecodeInstruction(code.data() + pos, code.size() - pos); + } catch (const Codegen::CodegenException&) { + firstInvalidPos = pos; + break; + } + if (!IsValid64BitInstruction(code, pos, info, tableVAddr, segmentVAddr, segmentEnd)) { + firstInvalidPos = pos; + break; + } + if (IsForbiddenSyscallInstruction(code, pos, info) || info.FlowKind != Codegen::ControlFlowKind::Sequential) return 0; + pos += info.Length; + } + if (!firstInvalidPos) return 0; + const bool duplicatedEightByteRow = code.size() >= 16 && std::memcmp(code.data(), code.data() + 8, 8) == 0; + if (*firstInvalidPos != 0 && (!duplicatedEightByteRow || *firstInvalidPos > 4)) return 0; + + std::optional firstDecodeFailure; + for (std::size_t pos = 0; pos < code.size();) { + if (analysis.OnlyPadding(pos)) break; + try { + const auto info = decoder.DecodeInstruction(code.data() + pos, code.size() - pos); + if (info.Length == 0 || info.Length > code.size() - pos) { + firstDecodeFailure = pos; + break; + } + if (IsForbiddenSyscallInstruction(code, pos, info)) return 0; + pos += info.Length; + } catch (const Codegen::CodegenException&) { + firstDecodeFailure = pos; + break; + } + } + if (!firstDecodeFailure) return 0; + + for (std::size_t pos = *firstDecodeFailure + 1; pos < code.size();) { + if (code[pos] != 0xcc && code[pos] != 0x90 && code[pos] != 0x00) { + ++pos; + continue; + } + const auto padStart = pos; + bool hasZeroPad = false; + while (pos < code.size() && (code[pos] == 0xcc || code[pos] == 0x90 || code[pos] == 0x00)) { + if (code[pos] == 0x00) hasZeroPad = true; + ++pos; + } + if (pos < code.size() && analysis.DecodesAsTrailingCodeBlock(pos)) + return hasZeroPad ? pos : padStart; + } + for (std::size_t pos = *firstDecodeFailure + 1; pos < code.size(); ++pos) { + bool hasSyscall = false; + if (analysis.DecodesAsTrailingCodeBlock(pos, &hasSyscall) && hasSyscall) + return pos; + } + return code.size(); +} + } std::size_t FindTrailingRelativeJumpTable(std::span code, VirtualAddress codeAddress, std::size_t* tableEnd, std::size_t unwindEnd) { @@ -121,6 +300,68 @@ std::size_t FindTrailingRelativeJumpTable(std::span code, Vi std::vector FindRelativeJumpTables(std::span segment, VirtualAddress segmentVAddr, const std::vector>& functions) { std::vector tables; const auto segmentEnd = segmentVAddr + segment.size(); + const Codegen::X64InstructionDecoder decoder; + std::set branchTargets; + for (const auto& [funcBegin, funcEnd] : functions) { + if (funcBegin < segmentVAddr || funcBegin >= segmentEnd || funcEnd <= funcBegin) continue; + const auto inspectFuncEnd = std::min(funcEnd, segmentEnd); + const auto funcCode = segment.subspan(funcBegin - segmentVAddr, inspectFuncEnd - funcBegin); + for (std::size_t pos = 0; pos < funcCode.size();) { + Codegen::DecodedInstructionInfo info; + try { + info = decoder.DecodeInstruction(funcCode.data() + pos, funcCode.size() - pos); + } catch (const Codegen::CodegenException&) { + break; + } + if (info.Length == 0 || info.Length > funcCode.size() - pos) break; + if (info.HasBranchTarget && !info.HasRipRelativeDisp) { + const auto target = static_cast(funcBegin + pos + info.Length) + info.BranchDisp; + if (target >= static_cast(segmentVAddr) && target < static_cast(segmentEnd)) + branchTargets.insert(static_cast(target)); + } + pos += info.Length; + } + } + auto knownFunctions = functions; + std::erase_if(knownFunctions, [](const auto& range) { return range.first >= range.second; }); + std::sort(knownFunctions.begin(), knownFunctions.end()); + std::vector> mergedFunctions; + for (const auto& range : knownFunctions) { + if (!mergedFunctions.empty() && range.first <= mergedFunctions.back().second) + mergedFunctions.back().second = std::max(mergedFunctions.back().second, range.second); + else + mergedFunctions.emplace_back(range); + } + std::vector pendingTargets(branchTargets.begin(), branchTargets.end()); + std::set exploredGaps; + while (!pendingTargets.empty()) { + const auto current = pendingTargets.back(); + pendingTargets.pop_back(); + if (current < segmentVAddr || current >= segmentEnd) continue; + const auto after = std::upper_bound(mergedFunctions.begin(), mergedFunctions.end(), current, + [](VirtualAddress value, const auto& range) { return value < range.first; }); + const bool inKnownFunction = after != mergedFunctions.begin() && current < std::prev(after)->second; + if (inKnownFunction || !exploredGaps.insert(current).second) continue; + auto pos = static_cast(current - segmentVAddr); + while (pos < segment.size()) { + Codegen::DecodedInstructionInfo info; + try { + info = decoder.DecodeInstruction(segment.data() + pos, segment.size() - pos); + } catch (const Codegen::CodegenException&) { + break; + } + if (info.Length == 0 || info.Length > segment.size() - pos) break; + if (info.HasBranchTarget && !info.HasRipRelativeDisp) { + const auto target = static_cast(segmentVAddr + pos + info.Length) + info.BranchDisp; + if (target >= static_cast(segmentVAddr) && target < static_cast(segmentEnd)) { + const auto targetVAddr = static_cast(target); + if (branchTargets.insert(targetVAddr).second) pendingTargets.push_back(targetVAddr); + } + } + if (info.FlowKind == Codegen::ControlFlowKind::Return || info.FlowKind == Codegen::ControlFlowKind::IndirectJump || info.FlowKind == Codegen::ControlFlowKind::Trap) break; + pos += info.Length; + } + } for (std::size_t index = 0; index < functions.size(); ++index) { const auto [begin, end] = functions[index]; if (begin < segmentVAddr || begin >= segmentEnd || end <= begin) continue; @@ -129,8 +370,45 @@ std::vector FindRelativeJumpTables(std::span candidateTable; + bool sawIndirectJump = false; + for (std::size_t pos = 0; pos < funcCode.size();) { + Codegen::DecodedInstructionInfo info; + try { + info = decoder.DecodeInstruction(funcCode.data() + pos, funcCode.size() - pos); + } catch (const Codegen::CodegenException&) { + break; + } + if (info.Length == 0 || info.Length > funcCode.size() - pos) break; + if (info.FlowKind == Codegen::ControlFlowKind::IndirectJump) sawIndirectJump = true; + const auto next = pos + info.Length; + if (info.HasRipRelativeDisp && !info.IsTwoByteOpcode && info.OpcodeOffset < info.Length && info.Length >= 4 && funcCode[pos + info.OpcodeOffset] == 0x8d) { + std::int32_t displacement = 0; + std::memcpy(&displacement, funcCode.data() + next - 4, sizeof(displacement)); + const auto target = static_cast(begin + next) + displacement; + if (target >= static_cast(end) && target < static_cast(inspectEnd)) { + const auto targetVAddr = static_cast(target); + candidateTable = candidateTable ? std::min(*candidateTable, targetVAddr) : targetVAddr; + } + } + pos = next; + } + if (sawIndirectJump || !candidateTable) continue; + const auto tableVAddr = *candidateTable; + const auto paddingCode = segment.subspan(end - segmentVAddr, tableVAddr - end); + if (!OnlyPadding(paddingCode, 0, paddingCode.size(), decoder)) continue; + const auto it = branchTargets.lower_bound(tableVAddr); + if (it != branchTargets.end() && *it < inspectEnd) continue; + const auto tableCode = segment.subspan(tableVAddr - segmentVAddr, inspectEnd - tableVAddr); + const auto tableSize = FindEmbeddedDataTableEnd(tableCode, tableVAddr, segmentVAddr, segmentEnd, decoder); + if (tableSize == 0) continue; + tables.push_back({begin, tableVAddr, tableVAddr + tableSize}); } return tables; } diff --git a/core/relinker/relinker/src/analysis/SyscallScanner.cpp b/core/relinker/relinker/src/analysis/SyscallScanner.cpp index 6003cda8e4..786e065c5e 100644 --- a/core/relinker/relinker/src/analysis/SyscallScanner.cpp +++ b/core/relinker/relinker/src/analysis/SyscallScanner.cpp @@ -128,27 +128,53 @@ static ForbiddenInstruction ClassifyForbiddenInstruction(const std::vector ConstantRaxLoad(const std::uint8_t* bytes, const std::size_t length) { +static std::optional ConstantRaxLoad(const std::uint8_t* bytes, const std::size_t length) { const std::size_t opcode = Codegen::DecodedInstruction{bytes, length}.OpcodeOffset(); const bool hasRex = opcode > 0 && (bytes[opcode - 1] & 0xF0) == 0x40; const std::uint8_t rex = hasRex ? bytes[opcode - 1] : 0; if (hasRex && rex == 0x48 && length - opcode == 6 && bytes[opcode] == 0xC7 && bytes[opcode + 1] == 0xC0) { - std::uint32_t value = 0; - std::memcpy(&value, bytes + opcode + 2, sizeof(value)); + std::int32_t immediate = 0; + std::memcpy(&immediate, bytes + opcode + 2, sizeof(immediate)); + return static_cast(static_cast(immediate)); + } + + if (hasRex && rex == 0x48 && length - opcode == 9 && bytes[opcode] == 0xB8) { + std::uint64_t value = 0; + std::memcpy(&value, bytes + opcode + 1, sizeof(value)); return value; } - if (!hasRex && length - opcode == 5 && bytes[opcode] == 0xB8) { + if ((rex & 0x09) == 0 && length - opcode == 5 && bytes[opcode] == 0xB8) { std::uint32_t value = 0; std::memcpy(&value, bytes + opcode + 1, sizeof(value)); return value; } + if ((rex & 0x05) == 0 && length - opcode == 2 && (bytes[opcode] == 0x31 || bytes[opcode] == 0x33) && bytes[opcode + 1] == 0xC0 + && std::find(bytes, bytes + opcode, 0x66) == bytes + opcode) + return 0; + return std::nullopt; } -static std::optional ConstantSyscallNumber(const std::vector& codeSection, const FileByteOffset codeSectionOffset, const RecentInstructions& recent) { +static bool MovesOtherRegisterThanRax(const std::uint8_t* bytes, const std::size_t length) { + const std::uint8_t rex = length == 3 ? bytes[0] : 0; + if (length == 3 && (rex & 0xF0) != 0x40) + return false; + const std::uint8_t opcode = bytes[length - 2]; + const std::uint8_t modrm = bytes[length - 1]; + if ((modrm & 0xC0) != 0xC0) + return false; + const bool writesRm = opcode == 0x89 || opcode == 0x31; + const bool writesReg = opcode == 0x8B || opcode == 0x33; + if (!writesRm && !writesReg) + return false; + const unsigned written = writesRm ? (modrm & 7u) | ((rex & 1u) << 3) : ((modrm >> 3) & 7u) | ((rex & 4u) << 1); + return written != 0; +} + +static std::optional ConstantSyscallNumber(const std::vector& codeSection, const FileByteOffset codeSectionOffset, const RecentInstructions& recent) { FileByteOffset next = recent.Before(0).Offset; for (std::size_t step = 1; step <= SYSCALL_NUMBER_LOOKBACK && step < recent.count; ++step) { const auto& match = recent.Before(step); @@ -166,11 +192,8 @@ static std::optional ConstantSyscallNumber(const std::vector 0 && (bytes[opcode - 1] & 0xF0) == 0x40; - const bool movesFirstSyscallArgument = hasRex && bytes[opcode - 1] == 0x49 - && match.Length - opcode == 2 && bytes[opcode] == 0x89 && bytes[opcode + 1] == 0xCA; - - if (!movesFirstSyscallArgument) + const std::size_t begin = opcode > 0 && (bytes[opcode - 1] & 0xF0) == 0x40 ? opcode - 1 : opcode; + if (match.Length - opcode != 2 || !MovesOtherRegisterThanRax(bytes + begin, match.Length - begin)) return std::nullopt; } @@ -211,22 +234,6 @@ static std::optional ImmediateLowRaxWriteEndingAt(const std::vector return std::nullopt; } -static bool MovesOtherRegisterThanRax(const std::uint8_t* bytes, const std::size_t length) { - const std::uint8_t rex = length == 3 ? bytes[0] : 0; - if (length == 3 && (rex & 0xF0) != 0x40) - return false; - const std::uint8_t opcode = bytes[length - 2]; - const std::uint8_t modrm = bytes[length - 1]; - if ((modrm & 0xC0) != 0xC0) - return false; - const bool writesRm = opcode == 0x89 || opcode == 0x31; - const bool writesReg = opcode == 0x8B || opcode == 0x33; - if (!writesRm && !writesReg) - return false; - const unsigned written = writesRm ? (modrm & 7u) | ((rex & 1u) << 3) : ((modrm >> 3) & 7u) | ((rex & 4u) << 1); - return written != 0; -} - static std::optional ConstantSyscallNumberInBytes(const std::vector& code, const std::size_t syscall) { using Step = std::tuple; std::vector pending{{syscall, 0, 0}}; diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp index d6a5eb9ffe..9108445515 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp @@ -175,7 +175,6 @@ std::unique_ptr BuildRelativeRelocationIndex( std::uint64_t segOff = read64(elfBytes, phPos + 8); std::uint64_t segSz = read64(elfBytes, phPos + 32); if (segOff > elfBytes.size() || segSz > elfBytes.size() - segOff) throw RelinkerException("PT_DYNAMIC segment out of bounds"); - if (segSz % 16 != 0) throw RelinkerException("Invalid dynamic segment size", segOff); std::optional relaVa; std::optional relaSz; diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp index 92b5f148e1..75d0da66f9 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp @@ -34,9 +34,8 @@ public: throw RelinkerException("Strict filter: invalid load segment size", segment.Offset); if (segment.Type != 2) continue; if (!tags.empty()) throw RelinkerException("Strict filter: multiple dynamic segments"); - if (segment.FileSize % 16 != 0) throw RelinkerException("Strict filter: truncated dynamic segment", segment.Offset); bool terminated = false; - for (std::uint64_t offset = 0; offset < segment.FileSize; offset += 16) { + for (std::uint64_t offset = 0; segment.FileSize - offset >= 16; offset += 16) { const auto tag = read(segment.Offset + offset); if (tag == 0) { terminated = true; diff --git a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp index 47d65ac011..b9d67a3069 100644 --- a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp +++ b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp @@ -11,7 +11,6 @@ void ValidationPolicy::_initializeSupportedRelocationTypes() { R_X86_64_NONE, R_X86_64_64, R_X86_64_PC32, - R_X86_64_COPY, R_X86_64_GLOB_DAT, R_X86_64_JUMP_SLOT, R_X86_64_RELATIVE, diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index 6010508de0..9e058f2b6e 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -38,8 +38,10 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector std::map loads; constexpr std::uint32_t sceComment = 0x6fffff00; constexpr std::uint32_t sceLibVersion = 0x6fffff01; + constexpr std::uint32_t note = 4; for (const auto& header : image.Headers) { if (header.Type == sceComment || header.Type == sceLibVersion) continue; + if (header.Type == note && header.MappedAddress == 0 && header.MemorySize == 0) continue; range(header.Offset, header.FileSize); if (header.Type == 1) { if (header.FileSize > header.MemorySize || header.MemorySize > std::numeric_limits::max() - header.MappedAddress || (header.Flags & ~7u) != 0) fail("Invalid load segment"); @@ -61,14 +63,14 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector tls = &header; } } - if (loads.empty() || dynamic == nullptr || dynamic->FileSize % 16 != 0) fail("Missing or invalid ELF load/dynamic segments"); + if (loads.empty() || dynamic == nullptr) fail("Missing or invalid ELF load/dynamic segments"); std::map tags; std::vector needed; std::vector moduleImports; std::vector ps4ModuleImports; std::vector moduleExports; bool terminated = false; - for (std::uint64_t offset = dynamic->Offset; offset < dynamic->Offset + dynamic->FileSize; offset += 16) { + for (std::uint64_t offset = dynamic->Offset; dynamic->Offset + dynamic->FileSize - offset >= 16; offset += 16) { const auto tag = Io::ReadU64(bytes, offset); const auto value = Io::ReadU64(bytes, offset + 8); if (tag == 0) { terminated = true; break; } @@ -189,11 +191,9 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector const auto [found, inserted] = exports.emplace(name, image.Symbols.size()); if (name.empty() || (!inserted && (!windows || !IdenticalOrdinaryFunctionExport(image.Symbols[found->second], symbol)))) fail("Duplicate or empty export after stripping #: " + name); } + if (section >= 0xff00 && (section != AbsoluteSection || (info & 15) == 6)) fail("Unsupported special symbol section: " + name); if ((info & 15) == 6 && section != 0 && (tls == nullptr || value > tls->MemorySize || size > tls->MemorySize - value)) fail("TLS symbol exceeds TLS segment: " + name); - if (section != 0 && section != AbsoluteSection && (info & 15) != 6) { - if (section >= 0xff00) fail("Unsupported special symbol section: " + name); - mapped(value, std::max(size, 1), (info & 15) == 2 ? 1 : 0); - } + if (section != 0 && section != AbsoluteSection && (info & 15) != 6) mapped(value, std::max(size, 1), (info & 15) == 2 ? 1 : 0); if (image.Dynamic.DynStrData.size() > std::numeric_limits::max()) fail("String table too large"); Io::WriteU32(image.Dynamic.DynSymData, offset, static_cast(image.Dynamic.DynStrData.size())); Io::AppendString(image.Dynamic.DynStrData, name); diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 31dc35aa0d..d234bbe4db 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -289,6 +290,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } if (provider != nullptr) { if ((symbol.Info & 15) != 0 && (symbol.Info & 15) != provider->Type) throw Domain::RelinkerException("Guest import/export type mismatch: " + symbol.Name); + if (provider->Type == 6 && (symbol.Info & 15) == 0 && referencedSymbols.contains(symbolIndex)) throw Domain::RelinkerException("Non-TLS relocation against guest TLS export: " + symbol.Name); if (provider->Provider != index && referencedSymbols.contains(symbolIndex)) dependencies[index].insert(provider->Provider); } else if (windows && (symbol.Info & 15) == 6) throw Domain::RelinkerException("Windows guest TLS import requires a guest TLS export: " + symbol.Name); if (provider == nullptr) systemImports[index].insert(symbolIndex); @@ -378,7 +380,12 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path }; for (const auto index : order) if (!windows && !deferred(index)) addNeeded("$ORIGIN/" + appDirectory(images[index]) + "/" + images[index].OutputName); for (const auto& name : hostLibraries) addNeeded(name); - if (!windows && runPath != "$ORIGIN" && !runPath.starts_with("$ORIGIN/") && !runPath.starts_with("$ORIGIN:") && !runPath.starts_with('/')) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); + if (!windows) for (std::size_t begin = 0; begin <= runPath.size();) { + const auto end = std::min(runPath.find(':', begin), runPath.size()); + const auto component = std::string_view(runPath).substr(begin, end - begin); + if (component != "$ORIGIN" && !component.starts_with("$ORIGIN/") && !component.starts_with('/')) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); + begin = end + 1; + } const auto outputDirectory = outputPath.parent_path().lexically_normal(); std::vector artifacts; for (const auto index : order) { @@ -394,14 +401,26 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } if (!image.PatchedRanges.empty()) GamePatch::CheckOverlaps(Io::Utf8Path(image.SourcePath), image.PatchedRanges, {.RelaData = image.Dynamic.RelaData, .RelaPltData = image.Dynamic.RelaPltData}); const bool sceImage = std::any_of(image.Headers.begin(), image.Headers.end(), [](const Domain::ProgramHeader& header) { return header.Type == 0x61000000; }); - const auto unwindFunctions = sceImage ? UnusedNidFilter::ReadExceptionFunctions(image.Bytes, image.Headers, {}, {}) : std::vector{}; + const bool hasEhFrame = sceImage || std::any_of(image.Headers.begin(), image.Headers.end(), [](const Domain::ProgramHeader& header) { return header.Type == 0x6474e550; }); + const auto unwindFunctions = hasEhFrame ? UnusedNidFilter::ReadExceptionFunctions(image.Bytes, image.Headers, {}, {}) : std::vector{}; + std::vector> unwindRanges; + for (const auto& function : unwindFunctions) unwindRanges.emplace_back(function.Begin, function.End); + std::sort(unwindRanges.begin(), unwindRanges.end()); for (const auto& header : codeHeaders) { std::vector code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize); if (sceImage) if (const auto end = FindSceCodeEnd(code, header.MappedAddress, unwindFunctions, image.Got, image.Dynamic.RelaPltData.size())) code.resize(*end - header.MappedAddress); - const auto unwindRanges = sceImage ? SceUnwindRanges(code, header.MappedAddress, unwindFunctions) : std::vector{}; - for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, code.size(), Io::Utf8Path(image.SourcePath), unwindRanges)) { + auto scanCode = code; + for (const auto& table : FindRelativeJumpTables(code, header.MappedAddress, unwindRanges)) { + if (table.Begin >= header.MappedAddress && table.End >= table.Begin) { + const auto maskEnd = std::min(table.End - header.MappedAddress, scanCode.size()); + const auto maskBegin = std::min(table.Begin - header.MappedAddress, maskEnd); + std::fill(scanCode.begin() + static_cast(maskBegin), scanCode.begin() + static_cast(maskEnd), std::uint8_t{0x90}); + } + } + const auto scanUnwindRanges = sceImage ? SceUnwindRanges(code, header.MappedAddress, unwindFunctions) : std::vector{}; + for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(scanCode, header.MappedAddress, scanCode.size(), Io::Utf8Path(image.SourcePath), scanUnwindRanges)) { if (offset > code.size() || code.size() - offset < 2u) throw Domain::RelinkerException("Syscall trap patch exceeds guest executable segment", header.MappedAddress + offset); if (!image.PatchedRanges.empty()) { diff --git a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp index a0044b4765..5c2881d32b 100644 --- a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp +++ b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp @@ -92,7 +92,8 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( if (ref.Nid.empty()) return static_cast((STB_WEAK << 4) | STT_NOTYPE); const bool tls = ref.RelocationTypeValue == R_X86_64_DTPMOD64 || ref.RelocationTypeValue == R_X86_64_DTPOFF64 || ref.RelocationTypeValue == R_X86_64_TPOFF64; - return static_cast(((ref.Weak ? STB_WEAK : STB_GLOBAL) << 4) | (tls ? STT_TLS : STT_FUNC)); + const auto type = tls ? STT_TLS : ref.SymbolType; + return static_cast(((ref.Weak ? STB_WEAK : STB_GLOBAL) << 4) | type); }; static constexpr std::size_t kRelaEntSize = 24; diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index 82f5e304ae..f112da1bea 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -270,15 +270,11 @@ std::vector ElfReader::ReadDynamicTags(const ProgramHeader& dynamicH if (!_rangeFits(dynamicHeader.Offset, dynamicHeader.FileSize, _fileBuffer.size())) { throw RelinkerException("Dynamic segment out of bounds", dynamicHeader.Offset); } - if (dynamicHeader.FileSize % 16 != 0) { - throw RelinkerException("Invalid dynamic segment size", dynamicHeader.Offset); - } - std::vector tags; FileByteOffset offset = dynamicHeader.Offset; const FileByteOffset end = dynamicHeader.Offset + dynamicHeader.FileSize; - while (offset < end) { + while (end - offset >= 16) { DynamicTag tag; tag.Tag = static_cast(_readU64At(offset)); tag.Value = _readU64At(offset + 0x08); diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp index 1f371ecbca..79a03f23bc 100644 --- a/core/relinker/relinker/src/pipeline/Conversion.cpp +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -155,7 +155,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti inputProgress.Message("Warning: with --lazy-binding the Linux executable stays position independent and is not placed at 0x400000; a title whose own sceLibcMspaceCreate checks the 40-bit address window stops at start-up\n"); std::shared_ptr patcher; if (macos) { - patcher = std::make_shared(options.MacosFixedBase, [&](std::string_view text) { inputProgress.Message(text); }); + patcher = std::make_shared(options.MacosFixedBase, [&](std::string_view text) { inputProgress.Message(text); }, options.TraceImports); } else if (windows) { patcher = std::make_shared(options.WindowsGui, std::move(input.Icon), input.InputPath.parent_path() / "sce_sys" / "icon0.png", [&](auto completed, auto total) { inputProgress.Report({Domain::ProgressStage::Image, {}, completed, total}); }, options.WindowsFixedBase, options.TraceImports, [&](std::string_view text) { inputProgress.Message(text); }); } else { diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 77044446d2..3ce5e70cb7 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -103,10 +103,11 @@ RelinkResult RelinkerPipeline::Relink() { for (const auto& ph : programHeaders) { if (ph.Type != PT_DYNAMIC) continue; + if (hasDynamicSegment) + throw RelinkerException("Multiple PT_DYNAMIC segments", ph.Offset); hasDynamicSegment = true; dynTags = elfReader->ReadDynamicTags(ph); - break; } if (!hasDynamicSegment) @@ -120,9 +121,6 @@ RelinkResult RelinkerPipeline::Relink() { }; auto getTagValue = [&](const std::int64_t tag) -> std::uint64_t { - for (const auto& t : dynTags) - if (t.Tag == tag) - return t.Value; const char* name = "unknown"; switch (tag) { case DT_OS_STRTAB: name = "DT_OS_STRTAB"; break; @@ -149,6 +147,16 @@ RelinkResult RelinkerPipeline::Relink() { case DT_RELAENT: name = "DT_RELAENT"; break; case DT_OS_SYMTABSZ: name = "DT_OS_SYMTABSZ"; break; } + std::optional value; + for (const auto& t : dynTags) { + if (t.Tag != tag) + continue; + if (value && *value != t.Value) + throw RelinkerException(std::string("Conflicting duplicate ") + name + " tags"); + value = t.Value; + } + if (value) + return *value; std::ostringstream message; message << "Required dynamic tag " << name << " (0x" << std::hex << tag << ") not found"; throw RelinkerException(message.str()); @@ -277,6 +285,8 @@ RelinkResult RelinkerPipeline::Relink() { for (auto& [fst, snd] : neededLibraryNamesByStrOffset) { snd = readCStr(fst); + if (snd.empty() || snd.find_first_of("\\:$\r\n") != std::string::npos) + throw RelinkerException("Invalid dependency: " + snd, dynStrTabOffset + fst); neededLibraries.push_back(snd); } @@ -312,8 +322,9 @@ RelinkResult RelinkerPipeline::Relink() { return type == R_X86_64_DTPMOD64 || type == R_X86_64_DTPOFF64 || type == R_X86_64_TPOFF64; }; - const bool hasTlsSegment = std::any_of(programHeaders.begin(), programHeaders.end(), - [](const ProgramHeader& ph) { return ph.Type == PT_TLS; }); + const auto tlsSegment = std::find_if(programHeaders.begin(), programHeaders.end(), + [](const ProgramHeader& ph) { return ph.Type == PT_TLS && ph.MemorySize != 0; }); + const bool hasNonEmptyTlsSegment = tlsSegment != programHeaders.end(); const auto definedTlsSymbolValue = [&](const std::uint32_t symIdx, const FileByteOffset pos) -> std::optional { const FileByteOffset symOff = symbolEntryPos(symIdx); @@ -325,6 +336,10 @@ RelinkResult RelinkerPipeline::Relink() { throw RelinkerException("TLS relocation names a defined symbol that is not STT_TLS", pos); std::uint64_t value = 0; std::memcpy(&value, raw.data() + symOff + 8, sizeof(value)); + std::uint64_t size = 0; + std::memcpy(&size, raw.data() + symOff + 16, sizeof(size)); + if (hasNonEmptyTlsSegment && (value > tlsSegment->MemorySize || size > tlsSegment->MemorySize - value)) + throw RelinkerException("TLS symbol exceeds TLS segment", pos); return value; }; const auto definedTlsSymbol = [&](const std::uint32_t symIdx, const FileByteOffset pos) { @@ -374,8 +389,9 @@ RelinkResult RelinkerPipeline::Relink() { [](const std::uint8_t byte) { return byte != 0; }))) throw RelinkerException("Unnamed import symbol is not a null symbol entry", symOff); const bool weak = (raw[symOff + 4] >> 4) == STB_WEAK; - const bool function = (raw[symOff + 4] & 15) == STT_FUNC; - nidRefs.push_back({name, Domain::ImportModule(name, importModules, neededLibraries), relType, pos, rOffset, rAddend, weak, function}); + const auto symbolType = static_cast(raw[symOff + 4] & 15); + const bool function = symbolType == STT_FUNC; + nidRefs.push_back({name, Domain::ImportModule(name, importModules, neededLibraries), relType, pos, rOffset, rAddend, weak, function, symbolType}); } }; @@ -384,17 +400,20 @@ RelinkResult RelinkerPipeline::Relink() { std::vector> unwindRanges; std::vector> segmentUnwindRanges(executableSegments.size()); - if (std::any_of(programHeaders.begin(), programHeaders.end(), [](const ProgramHeader& ph) { return ph.Type == PT_SCE_DYNLIBDATA; })) { + const bool sceImage = std::any_of(programHeaders.begin(), programHeaders.end(), [](const ProgramHeader& ph) { return ph.Type == PT_SCE_DYNLIBDATA; }); + const bool hasEhFrame = sceImage || std::any_of(programHeaders.begin(), programHeaders.end(), [](const ProgramHeader& ph) { return ph.Type == PT_GNU_EH_FRAME; }); + if (hasEhFrame) { const auto functions = UnusedNidFilter::ReadExceptionFunctions(raw, programHeaders, {}, {}); for (const auto& function : functions) unwindRanges.emplace_back(function.Begin, function.End); std::sort(unwindRanges.begin(), unwindRanges.end()); - for (std::size_t index = 0; index < executableSegments.size(); ++index) { - auto& [segment, segmentVAddr, segmentFileOffset] = executableSegments[index]; - static_cast(segmentFileOffset); - if (const auto codeEnd = FindSceCodeEnd(segment, segmentVAddr, functions, gotVAddr, dynJmpRelSize)) - segment.resize(*codeEnd - segmentVAddr); - segmentUnwindRanges[index] = SceUnwindRanges(segment, segmentVAddr, functions); - } + if (sceImage) + for (std::size_t index = 0; index < executableSegments.size(); ++index) { + auto& [segment, segmentVAddr, segmentFileOffset] = executableSegments[index]; + static_cast(segmentFileOffset); + if (const auto codeEnd = FindSceCodeEnd(segment, segmentVAddr, functions, gotVAddr, dynJmpRelSize)) + segment.resize(*codeEnd - segmentVAddr); + segmentUnwindRanges[index] = SceUnwindRanges(segment, segmentVAddr, functions); + } } std::vector> scanSegments; @@ -434,13 +453,42 @@ RelinkResult RelinkerPipeline::Relink() { std::memcpy(&sourceName, symbol, sizeof(sourceName)); auto name = readCStr(sourceName); name.resize(name.find('#') == std::string::npos ? name.size() : name.find('#')); + const FileByteOffset entryOffset = dynSymTabOffset + off; + const auto type = symbol[4] & 15; + std::uint64_t value = 0; + std::uint64_t size = 0; + std::memcpy(&value, symbol + 8, sizeof(value)); + std::memcpy(&size, symbol + 16, sizeof(size)); + if (type == STT_TLS) { + if (section >= SHN_LORESERVE) + throw RelinkerException("Unsupported executable TLS export section: " + name, entryOffset); + const auto tls = std::find_if(programHeaders.begin(), programHeaders.end(), + [](const ProgramHeader& ph) { return ph.Type == PT_TLS; }); + if (tls == programHeaders.end()) + throw RelinkerException("Executable TLS export has no PT_TLS segment: " + name, entryOffset); + if (value > tls->MemorySize || size > tls->MemorySize - value) + throw RelinkerException("Executable TLS export exceeds PT_TLS: " + name, entryOffset); + } else if (section != SHN_ABS) { + if (section >= SHN_LORESERVE) + throw RelinkerException("Unsupported executable export section: " + name, entryOffset); + const std::uint32_t flags = type == STT_FUNC ? PF_X : 0; + const auto extent = std::max(size, 1); + const bool mapped = std::any_of(programHeaders.begin(), programHeaders.end(), [&](const ProgramHeader& ph) { + return ph.Type == PT_LOAD && (ph.Flags & flags) == flags && value >= ph.MappedAddress && + value - ph.MappedAddress < ph.MemorySize && extent <= ph.MemorySize - (value - ph.MappedAddress); + }); + if (!mapped) { + std::ostringstream message; + message << "Executable export " << name << " at 0x" << std::hex << value + << (size > 1 ? " (" + std::to_string(size) + " bytes) does not fit inside " : " is not inside ") + << (flags != 0 ? "an executable" : "a") << " PT_LOAD segment"; + throw RelinkerException(message.str(), entryOffset); + } + } exportedSymbols.emplace_back(off, std::move(name)); const auto binding = symbol[4] >> 4; - if ((binding == STB_GLOBAL || binding == STB_WEAK) && (symbol[4] & 15) == STT_FUNC) { - VirtualAddress address = 0; - std::memcpy(&address, symbol + 8, sizeof(address)); - exportedFunctions.push_back(address); - } + if ((binding == STB_GLOBAL || binding == STB_WEAK) && type == STT_FUNC) + exportedFunctions.push_back(value); } } @@ -545,8 +593,8 @@ RelinkResult RelinkerPipeline::Relink() { continue; offset = *value; } - if (!hasTlsSegment) - throw RelinkerException("TLS relocation in an executable without a PT_TLS segment", pos); + if (!hasNonEmptyTlsSegment) + throw RelinkerException("TLS relocation in an executable without a non-empty PT_TLS segment", pos); if (relType == R_X86_64_DTPMOD64 && rAddend != 0) throw RelinkerException("DTPMOD64 relocation has a nonzero addend", pos); const auto addend = relType == R_X86_64_DTPMOD64 ? 0 : static_cast(offset + static_cast(rAddend)); diff --git a/core/relinker/relinker/tests/CallSiteResolverTests.cpp b/core/relinker/relinker/tests/CallSiteResolverTests.cpp index 6dd410e448..16ca224079 100644 --- a/core/relinker/relinker/tests/CallSiteResolverTests.cpp +++ b/core/relinker/relinker/tests/CallSiteResolverTests.cpp @@ -33,6 +33,28 @@ int main() { check("multiple REX prefixes on pointer load", {0x40, 0x48, 0x8b, 0x05, 0xf8, 0, 0, 0}, true); check("earlier REX.W does not widen a pointer load", {0x48, 0x40, 0x8b, 0x05, 0xf8, 0, 0, 0}, false); check("final REX.W widens a pointer load", {0x48, 0x40, 0x48, 0x8b, 0x05, 0xf7, 0, 0, 0}, true); + check("BND indirect call", {0xf2, 0xff, 0x15, 0xf9, 0, 0, 0}, true); + check("BND indirect tail jump", {0xf2, 0xff, 0x25, 0xf9, 0, 0, 0}, true); + check("NOTRACK indirect call", {0x3e, 0xff, 0x15, 0xf9, 0, 0, 0}, true); + check("NOTRACK indirect tail jump", {0x3e, 0xff, 0x25, 0xf9, 0, 0, 0}, true); + check("NOTRACK BND indirect call", {0x3e, 0xf2, 0xff, 0x15, 0xf8, 0, 0, 0}, true); + check("BND NOTRACK indirect tail jump", {0xf2, 0x3e, 0xff, 0x25, 0xf8, 0, 0, 0}, true); + check("BND REX indirect call", {0xf2, 0x48, 0xff, 0x15, 0xf8, 0, 0, 0}, true); + check("NOTRACK REX indirect tail jump", {0x3e, 0x48, 0xff, 0x25, 0xf8, 0, 0, 0}, true); + check("NOTRACK-prefixed pointer load", {0x3e, 0x48, 0x8b, 0x05, 0xf8, 0, 0, 0}, true); + check("REPNE-prefixed pointer load", {0xf2, 0x48, 0x8b, 0x05, 0xf8, 0, 0, 0}, true); + check("DS after REX.W leaves a narrow load", {0x48, 0x3e, 0x8b, 0x05, 0xf8, 0, 0, 0}, false); + check("REPNE after REX.W leaves a narrow load", {0x48, 0xf2, 0x8b, 0x05, 0xf8, 0, 0, 0}, false); + check("final REX.W after DS widens a load", {0x48, 0x3e, 0x48, 0x8b, 0x05, 0xf7, 0, 0, 0}, true); + check("final REX.W after REPNE widens a load", {0x48, 0xf2, 0x48, 0x8b, 0x05, 0xf7, 0, 0, 0}, true); + 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("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); + check("DS-prefixed decrement is not an import access", {0x3e, 0xff, 0x0d, 0xf9, 0, 0, 0}, false); check("pointer load without REX.W", {0x40, 0x8b, 0x05, 0xf9, 0, 0, 0}, false); check("register pointer load", {0x48, 0x8b, 0x00}, false); check("increment memory", {0xff, 0x05, 0xfa, 0, 0, 0}, false); diff --git a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp index 092f5fa398..aa79bae7f0 100644 --- a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp +++ b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace { @@ -364,6 +365,257 @@ void CheckRelativeJumpTables() { Require(Relinker::FindRelativeJumpTables(cutShort, TextAddress, {{TextAddress, TextAddress + cutShort.size()}}).empty(), "Table followed by more of its function was accepted"); } +void CheckEmbeddedDataTables() { + constexpr std::size_t tableOffset = 24; + Bytes text(48, 0x90); + text[0] = 0x48; + text[1] = 0x8d; + text[2] = 0x05; + SetDisplacement(text, 3, 7, TextAddress + tableOffset); + text[7] = 0xc3; + for (std::size_t i = 8; i < tableOffset; ++i) text[i] = 0xcc; + text[tableOffset] = 0x0f; + text[tableOffset + 1] = 0x78; + text[tableOffset + 2] = 0x33; + text[tableOffset + 3] = 0x1e; + + const std::vector> functions{ + {TextAddress, TextAddress + 8}, + {TextAddress + text.size(), TextAddress + text.size() + 8} + }; + const auto tables = Relinker::FindRelativeJumpTables(text, TextAddress, functions); + Require(tables.size() == 1, "Expected one embedded data table"); + Require(tables[0].Function == TextAddress && tables[0].Begin == TextAddress + tableOffset && tables[0].End == TextAddress + text.size(), "Embedded data table range was incorrect"); + + Bytes withBranch = text; + withBranch[7] = 0xe8; + SetDisplacement(withBranch, 8, 12, TextAddress + tableOffset); + withBranch[12] = 0xc3; + const std::vector> functionsWithBranch{ + {TextAddress, TextAddress + 13}, + {TextAddress + withBranch.size(), TextAddress + withBranch.size() + 8} + }; + const auto tablesWithBranch = Relinker::FindRelativeJumpTables(withBranch, TextAddress, functionsWithBranch); + Require(tablesWithBranch.empty(), "Branch target was incorrectly recognized as a data table"); + + Bytes withValidCode = text; + withValidCode[tableOffset] = 0x48; + withValidCode[tableOffset + 1] = 0x89; + withValidCode[tableOffset + 2] = 0xc0; + withValidCode[tableOffset + 3] = 0xc3; + for (std::size_t i = tableOffset + 4; i < withValidCode.size(); ++i) withValidCode[i] = 0xcc; + const auto tablesWithValidCode = Relinker::FindRelativeJumpTables(withValidCode, TextAddress, functions); + Require(tablesWithValidCode.empty(), "Valid unannotated code in gap was incorrectly recognized as a data table"); + + Bytes withTransitiveBranch = text; + withTransitiveBranch[7] = 0xe8; + SetDisplacement(withTransitiveBranch, 8, 12, TextAddress + 16); + withTransitiveBranch[12] = 0xc3; + for (std::size_t i = 13; i < 16; ++i) withTransitiveBranch[i] = 0xcc; + withTransitiveBranch[16] = 0xe9; + SetDisplacement(withTransitiveBranch, 17, 21, TextAddress + tableOffset); + for (std::size_t i = 21; i < tableOffset; ++i) withTransitiveBranch[i] = 0xcc; + const std::vector> functionsTransitive{ + {TextAddress, TextAddress + 13}, + {TextAddress + withTransitiveBranch.size(), TextAddress + withTransitiveBranch.size() + 8} + }; + const auto tablesTransitive = Relinker::FindRelativeJumpTables(withTransitiveBranch, TextAddress, functionsTransitive); + Require(tablesTransitive.empty(), "Transitive branch target was incorrectly recognized as a data table"); + + Bytes withTrailingCode(64, 0x90); + withTrailingCode[0] = 0x48; + withTrailingCode[1] = 0x8d; + withTrailingCode[2] = 0x05; + SetDisplacement(withTrailingCode, 3, 7, TextAddress + 16); + withTrailingCode[7] = 0xc3; + for (std::size_t i = 8; i < 16; ++i) withTrailingCode[i] = 0xcc; + withTrailingCode[16] = 0x0f; + withTrailingCode[17] = 0x78; + withTrailingCode[18] = 0x11; + withTrailingCode[19] = 0x22; + withTrailingCode[20] = 0x33; + withTrailingCode[21] = 0x44; + for (std::size_t i = 22; i < 32; ++i) withTrailingCode[i] = 0xcc; + withTrailingCode[32] = 0xb8; + withTrailingCode[33] = 0x3b; + withTrailingCode[34] = 0x00; + withTrailingCode[35] = 0x00; + withTrailingCode[36] = 0x00; + withTrailingCode[37] = 0x0f; + withTrailingCode[38] = 0x05; + withTrailingCode[39] = 0xc3; + for (std::size_t i = 40; i < withTrailingCode.size(); ++i) withTrailingCode[i] = 0x90; + const std::vector> functionsTrailing{ + {TextAddress, TextAddress + 8}, + {TextAddress + withTrailingCode.size(), TextAddress + withTrailingCode.size() + 8} + }; + const auto tablesTrailing = Relinker::FindRelativeJumpTables(withTrailingCode, TextAddress, functionsTrailing); + Require(tablesTrailing.size() == 1, "Expected one bounded data table before trailing code"); + Require(tablesTrailing[0].Begin == TextAddress + 16 && tablesTrailing[0].End <= TextAddress + 32, "Mask overran into trailing code"); + + Bytes withCallbackSyscall(48, 0x90); + withCallbackSyscall[0] = 0x48; + withCallbackSyscall[1] = 0x8d; + withCallbackSyscall[2] = 0x05; + SetDisplacement(withCallbackSyscall, 3, 7, TextAddress + 16); + withCallbackSyscall[7] = 0xc3; + for (std::size_t i = 8; i < 16; ++i) withCallbackSyscall[i] = 0xcc; + withCallbackSyscall[16] = 0xb8; + withCallbackSyscall[17] = 0x3b; + withCallbackSyscall[18] = 0x00; + withCallbackSyscall[19] = 0x00; + withCallbackSyscall[20] = 0x00; + withCallbackSyscall[21] = 0x0f; + withCallbackSyscall[22] = 0x05; + withCallbackSyscall[23] = 0x0f; + withCallbackSyscall[24] = 0x78; + withCallbackSyscall[25] = 0x11; + withCallbackSyscall[26] = 0x22; + withCallbackSyscall[27] = 0x33; + withCallbackSyscall[28] = 0x44; + withCallbackSyscall[29] = 0xc3; + const std::vector> functionsCallback{ + {TextAddress, TextAddress + 8}, + {TextAddress + withCallbackSyscall.size(), TextAddress + withCallbackSyscall.size() + 8} + }; + const auto tablesCallback = Relinker::FindRelativeJumpTables(withCallbackSyscall, TextAddress, functionsCallback); + Require(tablesCallback.empty(), "Callback with syscall and undecodable tail was incorrectly recognized as a data table"); + + Bytes whirlpoolText(4224, 0x90); + whirlpoolText[0] = 0x48; + whirlpoolText[1] = 0x8d; + whirlpoolText[2] = 0x2d; + SetDisplacement(whirlpoolText, 3, 7, TextAddress + 32); + whirlpoolText[7] = 0xc3; + for (std::size_t i = 8; i < 32; ++i) whirlpoolText[i] = 0xcc; + static constexpr std::uint8_t whirlpoolPrefix[]{ + 0x18, 0x18, 0x60, 0x18, 0xc0, 0x78, 0x30, 0xd8, 0x18, 0x18, 0x60, 0x18, 0xc0, 0x78, 0x30, 0xd8, + 0x0f, 0x78, 0x33, 0x1e + }; + std::memcpy(whirlpoolText.data() + 32, whirlpoolPrefix, sizeof(whirlpoolPrefix)); + const std::vector> functionsWhirlpool{ + {TextAddress, TextAddress + 8}, + {TextAddress + whirlpoolText.size(), TextAddress + whirlpoolText.size() + 8} + }; + const auto tablesWhirlpool = Relinker::FindRelativeJumpTables(whirlpoolText, TextAddress, functionsWhirlpool); + Require(tablesWhirlpool.size() == 1, "Expected Whirlpool-style table to be recognized"); + Require(tablesWhirlpool[0].Begin == TextAddress + 32, "Whirlpool table start was incorrect"); +} + +void CheckDecodedPaddingSuffix() { + Bytes text(0x38, 0x90); + text[0] = 0x48; + text[1] = 0x8d; + text[2] = 0x05; + SetDisplacement(text, 3, 7, TextAddress + 0x10); + text[7] = 0xc3; + for (std::size_t i = 8; i < 0x10; ++i) text[i] = 0xcc; + static constexpr std::uint8_t suffix[]{0x0f, 0x78, 0xcc, 0xb8, 0x09, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x00, 0x01, 0x90}; + std::memcpy(text.data() + 0x10, suffix, sizeof(suffix)); + text[0x30] = 0xc3; + const std::vector> functions{ + {TextAddress, TextAddress + 8}, + {TextAddress + 0x30, TextAddress + 0x38} + }; + const auto tables = Relinker::FindRelativeJumpTables(text, TextAddress, functions); + Require(tables.size() == 1, "Expected one decoded data table before a padding-suffixed syscall"); + Require(tables[0].Begin == TextAddress + 0x10 && tables[0].End == TextAddress + 0x12, "Mask extended over the syscall after a decoded padding suffix"); +} + +void CheckStubBeforeUndecodableData() { + Bytes text(0x38, 0x90); + text[0] = 0x48; + text[1] = 0x8d; + text[2] = 0x05; + SetDisplacement(text, 3, 7, TextAddress + 0x10); + text[7] = 0xc3; + for (std::size_t i = 8; i < 0x10; ++i) text[i] = 0xcc; + static constexpr std::uint8_t suffix[]{ + 0x0f, 0x78, 0xcc, 0xb8, 0x09, 0x00, 0x00, 0x00, + 0x0f, 0x05, 0xc3, 0x0f, 0x78, 0x33, 0x1e + }; + std::memcpy(text.data() + 0x10, suffix, sizeof(suffix)); + const std::vector> functions{ + {TextAddress, TextAddress + 8}, + {TextAddress + text.size(), TextAddress + text.size() + 8} + }; + const auto tables = Relinker::FindRelativeJumpTables(text, TextAddress, functions); + Require(tables.size() == 1, "Expected one data table before a syscall stub followed by undecodable data"); + Require(tables[0].Begin == TextAddress + 0x10, "Data table start before a syscall stub was incorrect"); + Require(tables[0].End <= TextAddress + 0x13, "Mask covered the syscall stub before undecodable data"); +} + +void CheckTableWithPaddingAndReturnBytes() { + constexpr std::size_t gap = 0x800; + Bytes text(0x10 + gap, 0x90); + text[0] = 0x48; + text[1] = 0x8d; + text[2] = 0x05; + SetDisplacement(text, 3, 7, TextAddress + 0x10); + text[7] = 0xc3; + for (std::size_t i = 8; i < 0x10; ++i) text[i] = 0xcc; + static constexpr std::uint8_t row[]{ + 0x18, 0x18, 0x60, 0x18, 0xc0, 0x78, 0x30, 0xd8, + 0x18, 0x18, 0x60, 0x18, 0xc0, 0x78, 0x30, 0xd8, + 0x0f, 0x78, 0x33, 0x1e + }; + for (std::size_t at = 0x10; at + sizeof(row) <= text.size(); at += sizeof(row)) + std::memcpy(text.data() + at, row, sizeof(row)); + static constexpr std::uint8_t paddedChain[]{0x00, 0xcc, 0x90, 0x48, 0x89, 0xc0, 0xc3}; + static constexpr std::uint8_t alignedChain[]{0x00, 0xcc, 0x90, 0x00, 0x48, 0x89, 0xc0, 0xc3}; + std::memcpy(text.data() + 0x10 + 0x3b8, paddedChain, sizeof(paddedChain)); + std::memcpy(text.data() + 0x10 + 0x4ac, alignedChain, sizeof(alignedChain)); + const std::vector> functions{ + {TextAddress, TextAddress + 8}, + {TextAddress + text.size(), TextAddress + text.size() + 8} + }; + const auto tables = Relinker::FindRelativeJumpTables(text, TextAddress, functions); + Require(tables.size() == 1, "Expected a table with padding and return bytes to be recognized"); + Require(tables[0].Begin == TextAddress + 0x10, "Table with padding and return bytes started at the wrong address"); + Require(tables[0].End == TextAddress + text.size(), "Mask stopped inside a table with padding and return bytes"); +} + +void CheckLargeZeroGaps() { + constexpr std::size_t stride = 0x10000; + constexpr std::size_t count = 32; + constexpr std::size_t functionSize = 8; + constexpr std::size_t codeFill = 0x8000; + Bytes text(stride * count, 0x00); + std::vector> functions; + for (std::size_t index = 0; index < count; ++index) { + const auto begin = index * stride; + text[begin] = 0x48; + text[begin + 1] = 0x8d; + text[begin + 2] = 0x05; + SetDisplacement(text, begin + 3, begin + 7, TextAddress + begin + functionSize); + text[begin + 7] = 0xc3; + const auto table = begin + functionSize; + text[table] = 0x0f; + text[table + 1] = 0x78; + text[table + 2] = 0x33; + text[table + 3] = 0x1e; + for (auto at = table + 4; at + 2 < begin + codeFill; at += 3) { + text[at] = 0x48; + text[at + 1] = 0x89; + text[at + 2] = 0xc0; + } + functions.emplace_back(TextAddress + begin, TextAddress + begin + functionSize); + } + functions.emplace_back(TextAddress + stride * count, TextAddress + stride * count + functionSize); + + const auto started = std::chrono::steady_clock::now(); + const auto tables = Relinker::FindRelativeJumpTables(text, TextAddress, functions); + const auto elapsed = std::chrono::steady_clock::now() - started; + + Require(tables.size() == count, "Expected one data table per large gap"); + for (std::size_t index = 0; index < tables.size(); ++index) { + Require(tables[index].Function == TextAddress + index * stride, "Large gap table was attached to the wrong function"); + Require(tables[index].Begin == TextAddress + index * stride + functionSize, "Large gap table start was incorrect"); + Require(tables[index].End == TextAddress + (index + 1) * stride, "Large gap table end was incorrect"); + } + Require(elapsed < std::chrono::seconds(10), "Large gap analysis took too long"); +} + } int main() { @@ -382,6 +634,11 @@ int main() { CheckAddressOverflow(); CheckHighVirtualAddress(); CheckRelativeJumpTables(); + CheckEmbeddedDataTables(); + CheckDecodedPaddingSuffix(); + CheckStubBeforeUndecodableData(); + CheckTableWithPaddingAndReturnBytes(); + CheckLargeZeroGaps(); std::cout << "Control-flow graph tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/relinker/relinker/tests/SyscallScannerTests.cpp b/core/relinker/relinker/tests/SyscallScannerTests.cpp index 65901a6f33..742627e104 100644 --- a/core/relinker/relinker/tests/SyscallScannerTests.cpp +++ b/core/relinker/relinker/tests/SyscallScannerTests.cpp @@ -76,6 +76,102 @@ const Bytes kSdkGenericStub = { 0x0F, 0x05, 0xC3, }; +const Bytes kSignExtendedStub = { + 0x48, 0xC7, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, + 0x49, 0x89, 0xCA, + 0x0F, 0x05, + 0xC3, +}; + +const Bytes kImm64Stub = { + 0x48, 0xB8, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x49, 0x89, 0xCA, + 0x0F, 0x05, + 0xC3, +}; + +const Bytes kImm64NegativeStub = { + 0x48, 0xB8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x49, 0x89, 0xCA, + 0x0F, 0x05, + 0xC3, +}; + +const Bytes kZeroedStub = { + 0x31, 0xC0, + 0x0F, 0x05, + 0xC3, +}; + +const Bytes kWideZeroedStub = { + 0x48, 0x31, 0xC0, + 0x0F, 0x05, + 0xC3, +}; + +const Bytes kWordZeroedStub = { + 0x66, 0x31, 0xC0, + 0x0F, 0x05, + 0xC3, +}; + +const Bytes kExtendedRegisterZeroedStub = { + 0x45, 0x31, 0xC0, + 0x0F, 0x05, + 0xC3, +}; + +const Bytes kExtendedSourceXorStub = { + 0x44, 0x31, 0xC0, + 0x0F, 0x05, + 0xC3, +}; + +const Bytes kExtendedIndexXorStub = { + 0x42, 0x31, 0xC0, + 0x0F, 0x05, + 0xC3, +}; + +const Bytes kLoadThenExtendedZeroedStub = { + 0xB8, 0x3B, 0x00, 0x00, 0x00, + 0x47, 0x31, 0xC0, + 0x0F, 0x05, + 0xC3, +}; + +const Bytes kPrefixAwareLoadStub = { + 0x44, 0xB8, 0x3B, 0x00, 0x00, 0x00, + 0x0F, 0x05, + 0xC3, +}; + +const Bytes kWordWideZeroedStub = { + 0x66, 0x48, 0x31, 0xC0, + 0x0F, 0x05, + 0xC3, +}; + +const Bytes kExtendedLoadStub = { + 0x41, 0xB8, 0x3B, 0x00, 0x00, 0x00, + 0x0F, 0x05, + 0xC3, +}; + +const Bytes kLoadThenMemoryLoadStub = { + 0xB8, 0x3B, 0x00, 0x00, 0x00, + 0x8B, 0x44, 0x8B, 0xF8, + 0x0F, 0x05, + 0xC3, +}; + +const Bytes kLoadThenImmediateAddStub = { + 0xB8, 0x3B, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x00, 0x31, 0xC9, + 0x0F, 0x05, + 0xC3, +}; + std::string RunScanner(const std::string& binary, const Bytes& code) { const TempDirectory directory; const auto input = directory.Path() / "input.elf"; @@ -141,11 +237,26 @@ int main(const int argc, char** argv) { RequireNumber(binary, kConstantStub, "syscall number 9", "64-bit constant stub"); RequireNumber(binary, kUnsignedStub, "syscall number 57", "32-bit constant stub"); RequireNumber(binary, kInt80Stub, "syscall number 11", "int 0x80 stub"); + RequireNumber(binary, kSignExtendedStub, "syscall number 18446744073709551615", "negative 32-bit constant stub"); + RequireNumber(binary, kImm64Stub, "syscall number 9", "64-bit immediate stub"); + RequireNumber(binary, kImm64NegativeStub, "syscall number 18446744073709551615", "negative 64-bit immediate stub"); + RequireNumber(binary, kZeroedStub, "syscall number 0", "zeroed stub"); + RequireNumber(binary, kWideZeroedStub, "syscall number 0", "wide zeroed stub"); + RequireNumber(binary, kExtendedIndexXorStub, "syscall number 0", "stub that zeroes eax with an index-only REX"); + RequireNumber(binary, kLoadThenExtendedZeroedStub, "syscall number 59", "stub whose intervening xor zeroes r8d"); + RequireNumber(binary, kPrefixAwareLoadStub, "syscall number 59", "32-bit constant stub with an index-only REX"); RequireNoNumber(binary, kRegisterStub, "stub that loads the number from a register"); RequireNoNumber(binary, kSysenterStub, "sysenter stub"); RequireNoNumber(binary, kSysretStub, "sysret stub with a constant load in front of it"); RequireNoNumber(binary, kClobberedStub, "stub whose intervening instruction writes rax"); + RequireNoNumber(binary, kWordZeroedStub, "stub that zeroes only ax"); + RequireNoNumber(binary, kWordWideZeroedStub, "stub whose wide zeroing xor carries an operand-size prefix"); + RequireNoNumber(binary, kExtendedRegisterZeroedStub, "stub that zeroes r8d"); + RequireNoNumber(binary, kExtendedSourceXorStub, "stub that xors eax with r8d"); + RequireNoNumber(binary, kExtendedLoadStub, "stub that loads r8d"); + RequireNoNumber(binary, kLoadThenMemoryLoadStub, "stub whose intervening instruction loads eax from memory"); + RequireNoNumber(binary, kLoadThenImmediateAddStub, "stub whose intervening instruction adds to eax"); RequireNumber(binary, kPrefixedConstantStub, "syscall number 9", "prefixed constant stub"); RequireNoNumber(binary, kPrefixedClobberedStub, "prefixed stub whose intervening instruction writes rax"); diff --git a/core/relinker/relinker/tests/test_conversion_diagnostics.py b/core/relinker/relinker/tests/test_conversion_diagnostics.py index 9e9daf8bdf..2bbc2b9dcd 100644 --- a/core/relinker/relinker/tests/test_conversion_diagnostics.py +++ b/core/relinker/relinker/tests/test_conversion_diagnostics.py @@ -75,7 +75,7 @@ def main(): convert(f"{index}-module-name", None, "Unsupported Windows guest filename", flags, module=guest_fixture(PLAIN_SITE)) module = guest_fixture(PLAIN_SITE) struct.pack_into(" second; entry/getargv arguments, Unicode, exact environment, cwd, ' + 'validation rejections and no exit handlers passed') + finally: + remove_tree(directory) + + +if __name__ == '__main__': + main() diff --git a/core/relinker/relinker/tests/test_executable_export_bounds.py b/core/relinker/relinker/tests/test_executable_export_bounds.py new file mode 100644 index 0000000000..0b5ea1a71c --- /dev/null +++ b/core/relinker/relinker/tests/test_executable_export_bounds.py @@ -0,0 +1,129 @@ +"""An exported symbol of the executable must lie inside the image it is exported from, the rule +GuestImageReader applies to every symbol of a guest module and the macOS target applied to the executable +alone. A defined symbol that is not local, hidden or internal must lie inside a PT_LOAD segment that is +executable for STT_FUNC, together with its size and at least one byte; a non-TLS export in SHN_ABS is exempt, +any other special section index is rejected, and an STT_TLS export must lie inside PT_TLS with a regular +section index. Symbols that are not exports are not checked. The check runs before any target-specific code, +so an export it rejects fails with the same line, at the symbol entry's file offset, on every target.""" + +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_linux_load_alignment import fixture + +PT_LOAD = 1 +PT_TLS = 7 +NOTYPE, OBJECT, FUNC, TLS = 0, 1, 2, 6 +LOCAL, GLOBAL, WEAK = 0, 1, 2 +SHN_ABS = 0xfff1 +SHN_SPECIAL = 0xff01 +TARGETS = {(): "linux", ("--lazy-binding",): "lazy", ("--windows",): "windows", ("--macos",): "macos"} +SYMBOL_OFFSET = 0x4900 + 24 +TLS_MEMSZ, TLS_ALIGN = 0x24, 16 + + +def case(name, kind, value, size, message=None, binding=GLOBAL, section=1, visibility=0, tls=False): + return name, kind, value, size, message, binding, section, visibility, tls + + +CASES = [ + case("func_in_text", FUNC, 0x200, 6), + case("object_in_data", OBJECT, 0x1100, 8), + case("object_fills_load_end", OBJECT, 0x1ff8, 8), + case("notype_zero_size_inside", NOTYPE, 0x1ff0, 0), + case("func_last_byte", FUNC, 0xfff, 0), + case("weak_object", OBJECT, 0x1100, 8, binding=WEAK), + case("absolute", OBJECT, 0x123456789, 8, section=SHN_ABS), + case("tls_inside", TLS, 0x10, 4, tls=True), + case("hidden_outside_ignored", OBJECT, 0x9000, 8, visibility=2), + case("local_outside_ignored", OBJECT, 0x9000, 8, binding=LOCAL), + case("internal_outside_ignored", OBJECT, 0x9000, 8, visibility=1), + case("tls_fills", TLS, 0x20, 4, tls=True), + case("func_outside", FUNC, 0x9000, 6, + "Executable export {} at 0x9000 (6 bytes) does not fit inside an executable PT_LOAD segment"), + case("object_outside", OBJECT, 0x9000, 8, + "Executable export {} at 0x9000 (8 bytes) does not fit inside a PT_LOAD segment"), + case("object_past_end", OBJECT, 0x1ffc, 8, + "Executable export {} at 0x1ffc (8 bytes) does not fit inside a PT_LOAD segment"), + case("object_straddles_text_end", OBJECT, 0xffc, 8, + "Executable export {} at 0xffc (8 bytes) does not fit inside a PT_LOAD segment"), + case("protected_outside", OBJECT, 0x9000, 8, + "Executable export {} at 0x9000 (8 bytes) does not fit inside a PT_LOAD segment", visibility=3), + case("marker_at_end", NOTYPE, 0x2000, 0, + "Executable export {} at 0x2000 is not inside a PT_LOAD segment"), + case("func_in_data", FUNC, 0x1100, 6, + "Executable export {} at 0x1100 (6 bytes) does not fit inside an executable PT_LOAD segment"), + case("func_straddles_text_end", FUNC, 0xffc, 8, + "Executable export {} at 0xffc (8 bytes) does not fit inside an executable PT_LOAD segment"), + case("func_zero_in_data", FUNC, 0x1100, 0, + "Executable export {} at 0x1100 is not inside an executable PT_LOAD segment"), + case("special_section", OBJECT, 0x1100, 8, + "Unsupported executable export section: {}", section=SHN_SPECIAL), + case("tls_beyond", TLS, 0x20, 8, + "Executable TLS export exceeds PT_TLS: {}", tls=True), + case("tls_without_segment", TLS, 0x10, 4, + "Executable TLS export has no PT_TLS segment: {}"), + case("tls_special_section", TLS, 0x10, 4, + "Unsupported executable TLS export section: {}", section=SHN_ABS, tls=True), +] + + +def image(name, kind, value, size, binding, section, visibility, tls): + strings = b"\0" + name.encode("ascii") + b"\0" + data = fixture() + struct.pack_into(" {expected ^ 1:02x} (flip one bit)\n" in result.stdout, result.stdout manifest = output.parent / "app.patches.toml" - text = manifest.read_text() + text = manifest.read_text(encoding="utf-8") assert 'main_target = "eboot.bin"' in text and 'name = "flip_bit"' in text, text assert 'main_target = "eboot.bin"\nlinux_base = 0x400000\n' in text, text assert f"Patch manifest: {manifest}\n" in result.stdout @@ -154,7 +154,7 @@ def main(): def relocation_overlap(relinker, work): def run(*arguments): - return subprocess.run([str(relinker), *arguments], capture_output=True, text=True, timeout=60) + return subprocess.run([str(relinker), *arguments], capture_output=True, text=True, encoding="utf-8", timeout=60) def patch_file(path, target, address, expected): path.write_text(f'format = 1\n\n[[patch]]\ntarget = "{target}"\nva = {address:#x}\nexpected = "{expected}"\nvalue = "ffff"\n') @@ -196,7 +196,7 @@ def relocation_overlap(relinker, work): def writer_overlap(relinker, work): def run(*arguments): - return subprocess.run([str(relinker), *arguments], capture_output=True, text=True, timeout=60) + return subprocess.run([str(relinker), *arguments], capture_output=True, text=True, encoding="utf-8", timeout=60) def attempt(name, module, options, address, expected, value="ff", target="sce_module/sample.prx"): case = work / name diff --git a/core/relinker/relinker/tests/test_guest_absolute_symbols.py b/core/relinker/relinker/tests/test_guest_absolute_symbols.py index 77a57f1fba..67fc6810a2 100644 --- a/core/relinker/relinker/tests/test_guest_absolute_symbols.py +++ b/core/relinker/relinker/tests/test_guest_absolute_symbols.py @@ -15,6 +15,7 @@ VALUE = 0x1234 SIZE = 0x10 ADDEND = 0x10 TARGET = 0x2320 +TLS_SECTIONS = (ABSOLUTE, COMMON, 0xFF00, 0xFFFF) def module_with_section(section, size=0): @@ -25,6 +26,16 @@ def module_with_section(section, size=0): return image +def tls_module(section, binding): + image = module_with_symbol(True) + struct.pack_into("> 32 + if index: + types[symbol_name(image, tags, index)] = image[symbols + index * 24 + 4] + return types + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-import-types-") as directory: + for relocation_type in (6, 7): + for binding in (1, 2): + for kind in range(16): + imports = [("FFFFFFFFFFF", 2), ("DDDDDDDDDDD", 1), ("NNNNNNNNNNN", kind)] + case = Path(directory) / f"{relocation_type}-{binding}-{kind}" + case.mkdir() + output = relink(relinker, case, importing_executable(imports, binding, relocation_type), {}) + symbols = imported_symbols(output.read_bytes()) + expected = {name: (binding << 4) | symbol_type for name, symbol_type in imports} + assert symbols == expected, (relocation_type, binding, kind, symbols, expected) + print("Import symbol type tests passed") + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_input_file.py b/core/relinker/relinker/tests/test_input_file.py index 6146103953..2124c7395e 100644 --- a/core/relinker/relinker/tests/test_input_file.py +++ b/core/relinker/relinker/tests/test_input_file.py @@ -21,7 +21,7 @@ def main(): output = work / (name + ".out") registry = work / (name + ".registry.json") result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", *flags, str(source), str(output)], - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) assert result.returncode == 2 and error in result.stderr and "bad_alloc" not in result.stderr, (name, flags, result) assert not output.exists() and not registry.exists(), (name, flags, result) diff --git a/core/relinker/relinker/tests/test_input_magic.py b/core/relinker/relinker/tests/test_input_magic.py index e26ccf337c..5a0292feb0 100644 --- a/core/relinker/relinker/tests/test_input_magic.py +++ b/core/relinker/relinker/tests/test_input_magic.py @@ -1,6 +1,7 @@ """Reject inputs that are not plain ELF files with a message that names what was read.""" from pathlib import Path +import os import platform import subprocess import sys @@ -42,8 +43,10 @@ def main(): resource.setrlimit(resource.RLIMIT_AS, (512 * 1024**2, 512 * 1024**2)) resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) + bounded_by_asan = "max_allocation_size_mb=" in os.environ.get("ASAN_OPTIONS", "") result = subprocess.run([str(relinker), "--skip-sce-module", str(source), str(output)], - capture_output=True, text=True, timeout=20, preexec_fn=limit_address_space) + capture_output=True, text=True, timeout=20, + preexec_fn=None if bounded_by_asan else limit_address_space) assert result.returncode == 2 and "Invalid ELF magic number: 7f 46 49 48" in result.stderr and not output.exists(), result print("Input magic diagnostics tests passed") diff --git a/core/relinker/relinker/tests/test_input_preservation.py b/core/relinker/relinker/tests/test_input_preservation.py index 241dd1f4bc..37f9ba92e9 100644 --- a/core/relinker/relinker/tests/test_input_preservation.py +++ b/core/relinker/relinker/tests/test_input_preservation.py @@ -30,7 +30,7 @@ def check_executable_hardlinks(relinker, work): links = protected.stat().st_nlink try: result = subprocess.run([str(relinker), *command], cwd=case, - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) assert result.returncode == 2 and "output would overwrite" in result.stderr, ( windows, output_name, input_name, readonly, result.stdout, result.stderr) for path, original in inputs.items(): @@ -61,7 +61,7 @@ def check_executable_hardlinks(relinker, work): path.chmod(stat.S_IREAD) try: result = subprocess.run([str(relinker), *command], cwd=case, - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) assert result.returncode == 0, (readonly, result.stdout, result.stderr) for name in ("executable", "secondary-one", "secondary-two"): assert outputs[name].read_bytes().startswith(b"MZ") @@ -111,7 +111,7 @@ def check_diagnostic_inputs(relinker, work): options.append("--registry") result = subprocess.run([str(relinker), "--windows", "--windows-diagnostics", *options, str(source), str(output)], cwd=diagnostics.parent, - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) owner = "an input module" if bundled else "the input executable" assert result.returncode == 2 and f"Windows import diagnostics output would overwrite {owner}" in result.stderr, (result.stdout, result.stderr) assert str(diagnostics) in result.stderr, result.stderr @@ -134,7 +134,7 @@ def check_diagnostic_inputs(relinker, work): options = ["--windows-diagnostics"] if enabled else [] result = subprocess.run([str(relinker), "--windows", "--skip-sce-module", *options, str(source), str(output)], cwd=case, - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) assert result.returncode == 0 and output.read_bytes().startswith(b"MZ"), (result.stdout, result.stderr) assert source.read_bytes() == original assert diagnostics.read_bytes() == (b"" if enabled else original) @@ -177,7 +177,7 @@ def check_registry_inputs(relinker, work, windows, needed=False): previous_output = b"previous executable" output.write_bytes(previous_output) result = subprocess.run([str(relinker), *options, "--registry", str(source), str(output)], - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) kind = "Needed libraries" if needed else "Registry" expected = f"{kind} output would overwrite an input module" if bundled else f"{kind} output would overwrite the input executable" assert result.returncode == 2 and expected in result.stderr, (result.returncode, result.stdout, result.stderr) @@ -204,7 +204,7 @@ def check_registry_after_failed_patch(relinker, work): for registry in registries: registry.write_bytes(previous_registry) result = subprocess.run([str(relinker), "--windows", "--registry", str(source), str(output)], - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) assert result.returncode == 2 and "Invalid Windows icon PNG" in result.stderr, (result.stdout, result.stderr) assert output.read_bytes() == previous_output assert all(registry.read_bytes() == previous_registry for registry in registries) @@ -225,7 +225,7 @@ def check_output_directory(relinker, work): output = str(target) if existing else str(target) + os.sep options = ["--windows"] if windows else [] result = subprocess.run([str(relinker), *options, str(source), output], cwd=case, - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) assert result.returncode == 2 and "Executable output is a directory" in result.stderr, (result.stdout, result.stderr) assert not (case / "app0").exists() and not (target / "app0").exists() assert target.exists() == existing @@ -273,7 +273,7 @@ def check_desktop_inputs(relinker, work): registry_path.write_bytes(previous_registry) options = ["--registry"] if registry else [] result = subprocess.run([str(relinker), *options, str(source), str(output)], - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) kind = "Desktop entry" if desktop else "Icon" owner = "an input module" if bundled else "the input executable" assert result.returncode == 2 and f"{kind} output would overwrite {owner}" in result.stderr, (result.stdout, result.stderr) @@ -332,7 +332,7 @@ def main(): arguments = [] if bundled else ["--skip-sce-module"] result = subprocess.run([str(relinker), *options, *arguments, "--registry", str(source), str(output)], - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) expected = "Executable output would overwrite an input module" if bundled else "Executable output would overwrite the input executable" if (result.returncode != 2 or expected not in result.stderr or source.read_bytes() != original @@ -365,7 +365,7 @@ def main(): if existing: output.write_bytes(b"previous output") result = subprocess.run([str(relinker), *options, "--registry", str(source), str(output)], - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) converted = case / "app0" / "sce_module" / "sample.prx.guest.prx" magic = b"MZ" if windows else b"\x7fELF" if (result.returncode != 0 or not output.read_bytes().startswith(magic) @@ -375,7 +375,7 @@ def main(): raise AssertionError((windows, existing, result.returncode, result.stdout, result.stderr)) if not windows: assert Path(f"{output}.png").read_bytes() == image - assert Path(f"{output}.desktop").read_text().startswith("[Desktop Entry]\n") + assert Path(f"{output}.desktop").read_text(encoding="utf-8").startswith("[Desktop Entry]\n") print("Input preservation tests passed") diff --git a/core/relinker/relinker/tests/test_linux_dynamic_strings.py b/core/relinker/relinker/tests/test_linux_dynamic_strings.py index 82c72cbbef..4dd3e1a453 100644 --- a/core/relinker/relinker/tests/test_linux_dynamic_strings.py +++ b/core/relinker/relinker/tests/test_linux_dynamic_strings.py @@ -56,7 +56,8 @@ def main(): source = Path(directory) / "input.elf" source.write_bytes(fixture()) for name, options, expected in (("default", [], b"$ORIGIN/libs"), - ("custom", ["--rpath", "/opt/anyps5/lib"], b"/opt/anyps5/lib")): + ("custom", ["--rpath", "/opt/anyps5/lib"], b"/opt/anyps5/lib"), + ("list", ["--rpath", "/opt/anyps5/lib:$ORIGIN/libs"], b"/opt/anyps5/lib:$ORIGIN/libs")): output = Path(directory) / (name + ".elf") result = subprocess.run([str(relinker), "--skip-sce-module", *options, str(source), str(output)], capture_output=True, text=True, timeout=20) @@ -70,6 +71,15 @@ def main(): raise AssertionError((name, "DT_RUNPATH is outside DT_STRSZ", run_path, tags[DT_STRSZ])) if dynstr_section_size(elf) != tags[DT_STRSZ]: raise AssertionError((name, ".dynstr size differs from DT_STRSZ", dynstr_section_size(elf), tags[DT_STRSZ])) + for index, run_path in enumerate(("", ":$ORIGIN/libs", "$ORIGIN/libs:", "/opt/a::/opt/b")): + for target in ([], ["--lazy-binding"]): + output = Path(directory) / f"empty-{index}-{len(target)}.elf" + result = subprocess.run([str(relinker), "--skip-sce-module", *target, "--rpath", run_path, str(source), str(output)], + capture_output=True, text=True, timeout=20) + if result.returncode != 2 or "Empty Linux run path component" not in result.stderr: + raise AssertionError((run_path, target, result.returncode, result.stdout, result.stderr)) + if output.exists(): + raise AssertionError((run_path, target, "an empty run path component created an output")) print("Linux dynamic string tests passed") diff --git a/core/relinker/relinker/tests/test_linux_eh_frame_header.py b/core/relinker/relinker/tests/test_linux_eh_frame_header.py index bb34e1e852..cfe4cf6053 100644 --- a/core/relinker/relinker/tests/test_linux_eh_frame_header.py +++ b/core/relinker/relinker/tests/test_linux_eh_frame_header.py @@ -18,7 +18,7 @@ def fixture(header_count, with_frame=True): image = bytearray(0x8000) image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) struct.pack_into("\n" + "#define _GNU_SOURCE\n#include \n" "#include \n" "void check(void) {\n" " int (*function)(void) = dlsym(RTLD_DEFAULT, \"FixtureExport\");\n" diff --git a/core/relinker/relinker/tests/test_linux_load_alignment.py b/core/relinker/relinker/tests/test_linux_load_alignment.py index 51008e9392..0a549b05ec 100644 --- a/core/relinker/relinker/tests/test_linux_load_alignment.py +++ b/core/relinker/relinker/tests/test_linux_load_alignment.py @@ -14,7 +14,7 @@ def fixture(): image = bytearray(0x8000) image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) struct.pack_into("\n" + "#include \n" + "static int replacement(void) { _exit(77); }\n" + "void* TraceImport_nid_no_patch(void* target, const char* name, const char* library) {\n" + " if (((unsigned long)__builtin_frame_address(0) & 15) != 0) _exit(13);\n" + " return strcmp(name, \"shared\") == 0 && strcmp(library, \"a.prx\") == 0 ? (void*)replacement : target;\n" + "}\n") +PLAIN = "int Unrelated(void) { return 0; }\n" +START = "void Aps5StartGuest_nid_no_patch(void (*entry)(void)) { entry(); }\n" + + +def relink(relinker, source, output, *options): + return subprocess.run([str(relinker), "--macos", "--skip-sce-module", *options, str(source), str(output)], + capture_output=True, text=True, timeout=30) + + +def library(work, name, code): + source = work / (name + ".c") + source.write_text(code) + output = work / (name + ".dylib") + subprocess.run(["xcrun", "clang", "-arch", "x86_64", "-dynamiclib", str(source), + "-Wl,-install_name,@rpath/" + name + ".prx", "-o", str(output)], + check=True, capture_output=True, timeout=20) + return output.read_bytes() + + +def run_binary(output): + command = (["/usr/bin/arch", "-x86_64"] if platform.machine() == "arm64" else []) + [str(output)] + return subprocess.run(command, capture_output=True, text=True, timeout=30) + + +def main(): + relinker = Path(sys.argv[1]).resolve() + runnable = sys.platform == "darwin" and platform.machine() in ("x86_64", "arm64") + with tempfile.TemporaryDirectory(prefix="anyps5-macos-trace-imports-") as directory: + work = Path(directory) + libraries = {} + if runnable: + libraries = {name: library(work, name, code) for name, code in ( + ("a", PROVIDER), ("hook", HOOK), ("plain", PLAIN), ("libkernel", START))} + + for name, options, libc, symbol_info, expected in ( + ("untraced", (), "hook", 0x12, 11), + ("traced", ("--trace-imports",), "hook", 0x12, 77), + ("fixed-base", ("--trace-imports", "--macos-fixed-base", "0x400000"), "hook", 0x12, 77), + ("data", ("--trace-imports",), "hook", 0x11, 11), + ("weak", ("--trace-imports",), "hook", 0x22, 11), + ("unsupported", ("--trace-imports",), "plain", 0x12, None)): + case = work / name + case.mkdir() + image = executable("a.prx", extra_dependencies=("libc.prx",)) + image[0x4898 + 4] = symbol_info + source = case / "input.elf" + source.write_bytes(image) + output = case / "output" + result = relink(relinker, source, output, *options) + assert result.returncode == 0, (name, result.stdout, result.stderr) + if not runnable: + continue + (case / "libs").mkdir() + (case / "libs" / "a.prx").write_bytes(libraries["a"]) + (case / "libs" / "libc.prx").write_bytes(libraries[libc]) + (case / "libs" / "libkernel.prx").write_bytes(libraries["libkernel"]) + output.chmod(0o755) + run = run_binary(output) + if run.returncode != expected and "Bad CPU type in executable" in run.stderr: + assert os.environ.get("GITHUB_ACTIONS") != "true", "macOS CI must execute the x86_64 import trace" + runnable = False + continue + if expected is None: + assert run.returncode != 0 and "TraceImport_nid_no_patch" in run.stderr, (name, run.returncode, run.stdout, run.stderr) + else: + assert run.returncode == expected, (name, run.returncode, run.stdout, run.stderr) + + case = work / "without-libc" + case.mkdir() + source = case / "input.elf" + source.write_bytes(executable("a.prx")) + result = relink(relinker, source, case / "output", "--trace-imports") + assert result.returncode == 2 and "--trace-imports requires libc.prx" in result.stderr, (result.returncode, result.stderr) + assert not (case / "output").exists() + result = relink(relinker, source, case / "output", "--trace-imports", "--lazy-binding") + assert result.returncode != 0 and "--trace-imports conflicts with --lazy-binding" in result.stderr, (result.returncode, result.stderr) + + print("macOS import tracing tests passed") + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_needed_libraries.py b/core/relinker/relinker/tests/test_needed_libraries.py index 9bf39efb8f..06a4ddb1d5 100644 --- a/core/relinker/relinker/tests/test_needed_libraries.py +++ b/core/relinker/relinker/tests/test_needed_libraries.py @@ -6,15 +6,15 @@ import sys import tempfile from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture -from test_guest_symbol_names import importing_executable +from test_guest_symbol_names import DYNAMIC, RELOCATIONS, STRINGS, SYMBOLS, importing_executable sys.path.insert(0, str(Path(__file__).resolve().parents[4] / "tools")) import import_audit -def named_module(tag): +def named_module(tag, name="Party.prx"): image = guest_fixture(PLAIN_SITE) - strings = b"\0Party.prx\0" + strings = b"\0" + name.encode() + b"\0" image[0x800:0x800 + len(strings)] = strings struct.pack_into(" FUNCTION else FUNCTION + FUNCTION_SIZE if functions is None: functions = [(EARLY_FUNCTION, FUNCTION_SIZE)] if early_function else [] functions.append((FUNCTION, FUNCTION_SIZE)) + if has_gap_embedded: + functions.append((LATER_FUNCTION, FUNCTION_SIZE)) interpreter = FUNCTION - len(INTERPRETER) image = bytearray(0x1000) image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) @@ -44,10 +49,10 @@ def fixture(sce_block=True, plt_push_offset=8, gap=0, interpreter_pointer=False, struct.pack_into(" 1;" in result.stdout, (name, result.stdout) + return True assert result.returncode == 0 and output.exists(), (name, result.returncode, result.stdout, result.stderr) registry = json.loads(output.with_suffix(".registry.json").read_text()) kept = len(registry) diff --git a/core/relinker/relinker/tests/test_windows_fixed_base.py b/core/relinker/relinker/tests/test_windows_fixed_base.py index 3d04fb54f9..cd35f8c65e 100644 --- a/core/relinker/relinker/tests/test_windows_fixed_base.py +++ b/core/relinker/relinker/tests/test_windows_fixed_base.py @@ -25,6 +25,12 @@ def file_offset(data, rva): raise AssertionError(f"RVA {rva:#x} has no file data") +def image_end(path): + data = path.read_bytes() + _, optional = pe_headers(data) + return struct.unpack_from(" predecessors; std::vector successors; std::vector dominators; diff --git a/core/shader/recompiler/ControlFlow/src/ControlFlowGraph.cpp b/core/shader/recompiler/ControlFlow/src/ControlFlowGraph.cpp index 3f331f1ece..22fe11817d 100644 --- a/core/shader/recompiler/ControlFlow/src/ControlFlowGraph.cpp +++ b/core/shader/recompiler/ControlFlow/src/ControlFlowGraph.cpp @@ -2,6 +2,7 @@ #include "ControlFlow/ControlFlowHelpers.hpp" #include #include +#include #include #include #include @@ -20,6 +21,7 @@ bool contains(const std::vector& values, std::uint32_t value) { std::vector intersectSorted(const std::vector& first, const std::vector& second) { std::vector result; + result.reserve(std::min(first.size(), second.size())); std::set_intersection(first.begin(), first.end(), second.begin(), second.end(), std::back_inserter(result)); return result; } @@ -43,7 +45,9 @@ void remapIds(std::vector& values, const std::vector()) != values.end()) { + sortUnique(values); + } } void rebuildPredecessors(ControlFlowGraph& graph) { @@ -118,18 +122,37 @@ bool ControlFlowGraph::PostDominates(std::uint32_t postDominator, std::uint32_t std::uint32_t ControlFlowGraph::FindNearestCommonPostDominator(std::uint32_t firstBlock, std::uint32_t secondBlock) const { const auto& first = FindBlock(firstBlock); const auto& second = FindBlock(secondBlock); + if (first.immediatePostDominator != InvalidControlFlowId && second.immediatePostDominator != InvalidControlFlowId) { + const BasicBlock* left = &first; + const BasicBlock* right = &second; + while (left->postDominators.size() > right->postDominators.size()) left = &FindBlock(left->immediatePostDominator); + while (right->postDominators.size() > left->postDominators.size()) right = &FindBlock(right->immediatePostDominator); + while (left != right) { + if (left->immediatePostDominator == left->id) { + return InvalidControlFlowId; + } + left = &FindBlock(left->immediatePostDominator); + right = &FindBlock(right->immediatePostDominator); + } + return left->id; + } const auto common = intersectSorted(first.postDominators, second.postDominators); if (common.empty()) { return InvalidControlFlowId; } + if (common.size() == 1u) { + return common.front(); + } for (const auto candidate : common) { - bool nearest = true; - for (const auto other : common) { - if (other != candidate && !PostDominates(other, candidate)) { - nearest = false; - break; - } + const auto& postDominators = FindBlock(candidate).postDominators; + if (postDominators.size() + 1u < common.size()) { + continue; } + auto member = postDominators.begin(); + const bool nearest = std::all_of(common.begin(), common.end(), [&](std::uint32_t other) { + member = std::lower_bound(member, postDominators.end(), other); + return other == candidate || (member != postDominators.end() && *member == other); + }); if (nearest) { return candidate; } diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 62014408a5..1a12c1702c 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -240,7 +240,7 @@ ConservativeZExport readConservativeZExport(Reader& reader) { ColorExportPacking readColorExportPacking(Reader& reader) { const auto value = reader.ReadU8(); - if (value > static_cast(ColorExportPacking::Unorm4_4_4_4)) { + if (value > static_cast(ColorExportPacking::Uint)) { throw std::runtime_error("RequestSerializer: invalid ColorExportPacking value"); } return static_cast(value); diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index 8684767d4a..19da8b5235 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -52,6 +52,83 @@ bool inPlaceIntersections() { return enabled; } +bool dominatorTrees() { + static const bool enabled = std::getenv("APS5_NO_PERF_DOMINATOR_TREE") == nullptr; + return enabled; +} + +template +bool assignTreeDominators(std::vector& blocks, const std::vector& roots, IncomingOf incomingOf, std::vector BasicBlock::*sets, std::uint32_t BasicBlock::*parent = nullptr) { + const auto count = static_cast(blocks.size()); + const auto virtualRoot = count; + std::vector isRoot(count, false); + std::vector> outgoing(count + 1u); + for (const auto root : roots) { + isRoot[root] = true; + outgoing[virtualRoot].push_back(root); + } + for (std::uint32_t id = 0; id < count; ++id) { + if (isRoot[id]) continue; + for (const auto from : incomingOf(id)) outgoing[from].push_back(id); + } + std::vector postOrder(count + 1u, InvalidControlFlowId); + std::vector order; + order.reserve(count + 1u); + std::vector> stack = {{virtualRoot, 0u}}; + postOrder[virtualRoot] = 0; + while (!stack.empty()) { + auto& [node, next] = stack.back(); + if (next < outgoing[node].size()) { + const auto target = outgoing[node][next++]; + if (postOrder[target] == InvalidControlFlowId) { + postOrder[target] = 0; + stack.emplace_back(target, 0u); + } + continue; + } + postOrder[node] = static_cast(order.size()); + order.push_back(node); + stack.pop_back(); + } + if (order.size() != count + 1u) { + return false; + } + std::vector immediate(count + 1u, InvalidControlFlowId); + immediate[virtualRoot] = virtualRoot; + const auto intersect = [&](std::uint32_t first, std::uint32_t second) { + while (first != second) { + while (postOrder[first] < postOrder[second]) first = immediate[first]; + while (postOrder[second] < postOrder[first]) second = immediate[second]; + } + return first; + }; + for (bool changed = true; changed;) { + changed = false; + for (auto position = order.size() - 1u; position-- > 0u;) { + const auto node = order[position]; + auto next = isRoot[node] ? virtualRoot : InvalidControlFlowId; + if (!isRoot[node]) { + for (const auto from : incomingOf(node)) { + if (immediate[from] != InvalidControlFlowId) next = next == InvalidControlFlowId ? from : intersect(from, next); + } + } + if (immediate[node] != next) { + immediate[node] = next; + changed = true; + } + } + } + for (auto position = order.size() - 1u; position-- > 0u;) { + const auto node = order[position]; + auto& set = blocks[node].*sets; + if (immediate[node] == virtualRoot) set.clear(); + else set = blocks[immediate[node]].*sets; + set.insert(std::upper_bound(set.begin(), set.end(), node), node); + if (parent != nullptr) blocks[node].*parent = immediate[node] == virtualRoot ? node : immediate[node]; + } + return true; +} + struct BlockSet { std::vector words; @@ -91,6 +168,10 @@ struct BlockSet { words[static_cast(id) / 64u] |= std::uint64_t{1} << (id % 64u); } + bool has(std::uint32_t id) const { + return (words[static_cast(id) / 64u] >> (id % 64u) & 1u) != 0u; + } + void intersect(const BlockSet& other) { for (std::size_t i = 0; i < words.size(); ++i) words[i] &= other.words[i]; } @@ -160,6 +241,10 @@ void addSortedUnique(std::vector& values, std::uint32_t value) { } } +bool containsSorted(const std::vector& values, std::uint32_t value) { + return std::binary_search(values.begin(), values.end(), value); +} + bool replaceValue(std::vector& values, std::uint32_t oldValue, std::uint32_t newValue) { bool changed = false; for (auto& value : values) { @@ -203,6 +288,7 @@ std::vector applyBlockOrder(ControlFlowGraph& graph, std::vector< graph.entryBlock = remapId(graph.entryBlock, idMap); for (auto& block : graph.blocks) { block.id = remapId(block.id, idMap); + block.immediatePostDominator = remapId(block.immediatePostDominator, idMap); remapIds(block.predecessors, idMap); remapIds(block.successors, idMap); remapIds(block.dominators, idMap); @@ -248,10 +334,11 @@ std::uint32_t moveBlockBefore(ControlFlowGraph& graph, std::uint32_t blockId, st std::vector dominatedBlocks(const ControlFlowGraph& graph, std::uint32_t headerBlock, std::uint32_t stopBlock = InvalidControlFlowId) { std::vector blocks; + if (headerBlock == stopBlock || !graph.Dominates(headerBlock, headerBlock)) { + return blocks; + } std::vector stack = {headerBlock}; std::vector visited(graph.blocks.size()); - blocks.reserve(graph.blocks.size()); - stack.reserve(graph.blocks.size()); while (!stack.empty()) { const auto blockId = stack.back(); @@ -259,7 +346,7 @@ std::vector dominatedBlocks(const ControlFlowGraph& graph, std::u if (blockId >= visited.size()) { visited.resize(blockId + 1); } - if (blockId == stopBlock || visited[blockId] || !graph.Dominates(headerBlock, blockId)) { + if (visited[blockId]) { continue; } @@ -267,7 +354,7 @@ std::vector dominatedBlocks(const ControlFlowGraph& graph, std::u visited[blockId] = true; blocks.push_back(blockId); for (const auto successor : block.successors) { - if (successor != stopBlock && graph.Dominates(headerBlock, successor)) { + if (successor != stopBlock && (successor >= visited.size() || !visited[successor]) && graph.Dominates(headerBlock, successor)) { stack.push_back(successor); } } @@ -319,7 +406,7 @@ bool isolateSemanticLoopHeader(ControlFlowGraph& graph, std::uint32_t oldHeader, const NaturalLoop* findInnermostContainingLoop(const ControlFlowGraph& graph, std::uint32_t blockId) { const NaturalLoop* innermost = nullptr; for (const auto& loop : graph.naturalLoops) { - if (contains(loop.blocks, blockId) && (innermost == nullptr || loop.blocks.size() < innermost->blocks.size())) { + if (containsSorted(loop.blocks, blockId) && (innermost == nullptr || loop.blocks.size() < innermost->blocks.size())) { innermost = &loop; } } @@ -811,8 +898,8 @@ bool isInnermostLoopControlConditional(const ControlFlowGraph& graph, const Basi }; return isRepeatTarget(trueTarget) && isRepeatTarget(falseTarget); } - const bool trueInBody = contains(loop->blocks, trueTarget); - const bool falseInBody = contains(loop->blocks, falseTarget); + const bool trueInBody = containsSorted(loop->blocks, trueTarget); + const bool falseInBody = containsSorted(loop->blocks, falseTarget); if (trueInBody != falseInBody) { return true; } @@ -832,8 +919,8 @@ std::uint32_t exitTailSelectionMerge(const ControlFlowGraph& graph, const BasicB } const auto trueTarget = block.terminator.trueBlock; const auto falseTarget = block.terminator.falseBlock; - const bool trueInBody = contains(loop->blocks, trueTarget); - if (trueInBody == contains(loop->blocks, falseTarget)) { + const bool trueInBody = containsSorted(loop->blocks, trueTarget); + if (trueInBody == containsSorted(loop->blocks, falseTarget)) { return InvalidControlFlowId; } const auto body = trueInBody ? trueTarget : falseTarget; @@ -854,7 +941,7 @@ std::uint32_t exitTailSelectionMerge(const ControlFlowGraph& graph, const BasicB if (successor == loop->mergeBlock) { continue; } - if (contains(loop->blocks, successor) || !graph.Dominates(tail, successor) || !isInsideLoopConstruct(graph, *loop, successor)) { + if (containsSorted(loop->blocks, successor) || !graph.Dominates(tail, successor) || !isInsideLoopConstruct(graph, *loop, successor)) { return InvalidControlFlowId; } pending.push_back(successor); @@ -886,8 +973,9 @@ bool mergeLeavesContainingLoop(const ControlFlowGraph& graph, std::uint32_t head return false; } -bool splitSharedMergeBlock(ControlFlowGraph& graph, std::uint32_t merge, const std::vector& constructBlocks, bool forceSplit = false) { - if (merge == InvalidControlFlowId || merge >= graph.blocks.size() || contains(constructBlocks, merge)) { +template +bool splitSharedMergeBlock(ControlFlowGraph& graph, std::uint32_t merge, InConstruct inConstruct, bool forceSplit = false) { + if (merge == InvalidControlFlowId || merge >= graph.blocks.size() || inConstruct(merge)) { return false; } @@ -895,7 +983,7 @@ bool splitSharedMergeBlock(ControlFlowGraph& graph, std::uint32_t merge, const s std::vector constructPredecessors; bool hasExternalPredecessor = false; for (const auto predecessor : mergeBlock.predecessors) { - if (contains(constructBlocks, predecessor)) { + if (inConstruct(predecessor)) { addUnique(constructPredecessors, predecessor); } else { hasExternalPredecessor = true; @@ -928,6 +1016,29 @@ bool splitSharedMergeBlock(ControlFlowGraph& graph, std::uint32_t merge, const s return true; } +bool splitSharedMergeBlock(ControlFlowGraph& graph, std::uint32_t merge, const std::vector& constructBlocks, bool forceSplit = false) { + return splitSharedMergeBlock(graph, merge, [&](std::uint32_t blockId) { return containsSorted(constructBlocks, blockId); }, forceSplit); +} + +bool constructsFollowDominance(const ControlFlowGraph& graph) { + if (!inPlaceIntersections() || !dominatorTrees() || graph.entryBlock >= graph.blocks.size() || reversePostOrder(graph).size() != graph.blocks.size()) { + return false; + } + for (const auto& block : graph.blocks) { + for (const auto successor : block.successors) { + if (!containsSorted(graph.blocks[successor].predecessors, block.id)) { + return false; + } + } + for (const auto predecessor : block.predecessors) { + if (!contains(graph.blocks[predecessor].successors, block.id)) { + return false; + } + } + } + return true; +} + bool splitOneReturnJoin(ControlFlowGraph& graph, bool relaxReturnJoins) { for (const auto& block : graph.blocks) { if (block.terminator.kind != TerminatorKind::ConditionalBranch || findInnermostContainingLoop(graph, block.id) != nullptr || findSelectionMerge(graph, block) != InvalidControlFlowId) { @@ -962,21 +1073,21 @@ bool splitOneLoopMerge(ControlFlowGraph& graph) { } std::vector selectionRegion(const ControlFlowGraph& graph, const BasicBlock& header, std::uint32_t merge) { - std::vector region; + const auto count = static_cast(graph.blocks.size()); + auto region = BlockSet::none(count); std::vector pending = {header.terminator.trueBlock, header.terminator.falseBlock}; const auto* loop = findInnermostContainingLoop(graph, header.id); while (!pending.empty()) { const auto blockId = pending.back(); pending.pop_back(); - if (blockId == merge || contains(region, blockId) || (loop != nullptr && (blockId == loop->mergeBlock || blockId == loop->continueBlock))) { + if (blockId == merge || (blockId < count && region.has(blockId)) || (loop != nullptr && (blockId == loop->mergeBlock || blockId == loop->continueBlock))) { continue; } const auto& block = graph.FindBlock(blockId); - addUnique(region, blockId); + region.add(blockId); pending.insert(pending.end(), block.successors.begin(), block.successors.end()); } - sortUnique(region); - return region; + return region.ids(); } constexpr std::uint32_t CloneWordLimit = 1024u; @@ -1034,9 +1145,11 @@ using Edge = std::pair; std::vector externalEntryEdges(const ControlFlowGraph& graph, std::uint32_t header, const std::vector& region) { std::vector edges; + const auto count = static_cast(graph.blocks.size()); + const auto inRegion = BlockSet::fromIds(count, region); for (const auto member : region) { for (const auto predecessor : graph.FindBlock(member).predecessors) { - if (predecessor == header || contains(region, predecessor)) { + if (predecessor == header || (predecessor < count && inRegion.has(predecessor))) { continue; } if (graph.Dominates(member, predecessor)) { @@ -1050,6 +1163,7 @@ std::vector externalEntryEdges(const ControlFlowGraph& graph, std::uint32_ std::vector reachableWithin(const ControlFlowGraph& graph, const std::vector& entryEdges, const std::vector& region) { std::vector reached; + std::vector isReached(graph.blocks.size(), false); std::vector pending; for (const auto& edge : entryEdges) { pending.push_back(edge.second); @@ -1057,12 +1171,13 @@ std::vector reachableWithin(const ControlFlowGraph& graph, const while (!pending.empty()) { const auto blockId = pending.back(); pending.pop_back(); - if (contains(reached, blockId)) { + if (isReached[blockId]) { continue; } + isReached[blockId] = true; reached.push_back(blockId); for (const auto successor : graph.FindBlock(blockId).successors) { - if (contains(region, successor)) { + if (containsSorted(region, successor)) { pending.push_back(successor); } } @@ -1131,6 +1246,7 @@ void cloneBlocks(ControlFlowGraph& graph, const std::vector& bloc copy.predecessors.clear(); copy.dominators.clear(); copy.postDominators.clear(); + copy.immediatePostDominator = InvalidControlFlowId; for (const auto& [original, clone] : cloneOf) { redirectEdge(copy, original, clone); } @@ -1494,15 +1610,17 @@ bool splitOneSelectionMerge(ControlFlowGraph& graph, SplitBudget& budget, const std::vector loopHeaders; loopHeaders.reserve(graph.naturalLoops.size()); for (const auto& loop : graph.naturalLoops) { - addUnique(loopHeaders, loop.headerBlock); + loopHeaders.push_back(loop.headerBlock); } + sortUnique(loopHeaders); std::vector selectionHeaders; for (const auto& block : graph.blocks) { - if (block.terminator.kind == TerminatorKind::ConditionalBranch && !contains(loopHeaders, block.id)) { + if (block.terminator.kind == TerminatorKind::ConditionalBranch && !containsSorted(loopHeaders, block.id)) { selectionHeaders.push_back(block.id); } } + const bool dominanceConstructs = constructsFollowDominance(graph); std::sort(selectionHeaders.begin(), selectionHeaders.end(), [&](std::uint32_t lhs, std::uint32_t rhs) { const auto lhsDepth = graph.FindBlock(lhs).dominators.size(); const auto rhsDepth = graph.FindBlock(rhs).dominators.size(); @@ -1525,6 +1643,13 @@ bool splitOneSelectionMerge(ControlFlowGraph& graph, SplitBudget& budget, const return true; } + if (dominanceConstructs && merge != blockId && graph.Dominates(blockId, blockId) && !graph.Dominates(merge, blockId)) { + const auto inConstruct = [&](std::uint32_t member) { return member != merge && graph.Dominates(blockId, member) && !graph.Dominates(merge, member); }; + if (splitSharedMergeBlock(graph, merge, inConstruct, mergeLeavesContainingLoop(graph, blockId, merge))) { + return true; + } + continue; + } const auto constructBlocks = dominatedBlocks(graph, blockId, merge); const auto forceSplit = mergeLeavesContainingLoop(graph, blockId, merge); if (splitSharedMergeBlock(graph, merge, constructBlocks, forceSplit)) { @@ -1536,10 +1661,13 @@ bool splitOneSelectionMerge(ControlFlowGraph& graph, SplitBudget& budget, const std::vector naturalLoopBody(const ControlFlowGraph& graph, std::uint32_t headerBlock, std::uint32_t latchBlock, bool& natural) { std::vector body; + std::vector inBody(graph.blocks.size(), false); std::vector stack; natural = true; addUnique(body, headerBlock); addUnique(body, latchBlock); + inBody[headerBlock] = true; + inBody[latchBlock] = true; if (latchBlock != headerBlock) { stack.push_back(latchBlock); } @@ -1552,7 +1680,8 @@ std::vector naturalLoopBody(const ControlFlowGraph& graph, std::u if (!graph.Dominates(headerBlock, predecessor)) { natural = false; } - if (!contains(body, predecessor)) { + if (!inBody[predecessor]) { + inBody[predecessor] = true; body.push_back(predecessor); if (predecessor != headerBlock) { stack.push_back(predecessor); @@ -1575,27 +1704,7 @@ struct TarjanState { std::vector components; }; -void tarjanVisit(TarjanState& state, std::uint32_t blockId) { - state.index[blockId] = state.nextIndex; - state.lowlink[blockId] = state.nextIndex; - ++state.nextIndex; - state.stack.push_back(blockId); - state.onStack[blockId] = true; - - const auto& block = state.graph->FindBlock(blockId); - for (const auto successor : block.successors) { - if (state.index[successor] == InvalidControlFlowId) { - tarjanVisit(state, successor); - state.lowlink[blockId] = std::min(state.lowlink[blockId], state.lowlink[successor]); - } else if (state.onStack[successor]) { - state.lowlink[blockId] = std::min(state.lowlink[blockId], state.index[successor]); - } - } - - if (state.lowlink[blockId] != state.index[blockId]) { - return; - } - +void emitComponent(TarjanState& state, std::uint32_t blockId) { StronglyConnectedComponent component; for (;;) { const auto member = state.stack.back(); @@ -1615,7 +1724,7 @@ void tarjanVisit(TarjanState& state, std::uint32_t blockId) { cyclic = true; } for (const auto predecessor : memberBlock.predecessors) { - if (!contains(component.blocks, predecessor)) { + if (!containsSorted(component.blocks, predecessor)) { addUnique(component.entryBlocks, member); } } @@ -1625,6 +1734,45 @@ void tarjanVisit(TarjanState& state, std::uint32_t blockId) { state.components.push_back(std::move(component)); } +void tarjanVisit(TarjanState& state, std::uint32_t root) { + struct Frame { + std::uint32_t blockId; + const std::vector* successors; + std::size_t next; + }; + std::vector work; + const auto enter = [&](std::uint32_t blockId) { + state.index[blockId] = state.nextIndex; + state.lowlink[blockId] = state.nextIndex; + ++state.nextIndex; + state.stack.push_back(blockId); + state.onStack[blockId] = true; + work.push_back({blockId, &state.graph->FindBlock(blockId).successors, 0u}); + }; + enter(root); + while (!work.empty()) { + auto& frame = work.back(); + const auto blockId = frame.blockId; + if (frame.next < frame.successors->size()) { + const auto successor = (*frame.successors)[frame.next++]; + if (state.index[successor] == InvalidControlFlowId) { + enter(successor); + } else if (state.onStack[successor]) { + state.lowlink[blockId] = std::min(state.lowlink[blockId], state.index[successor]); + } + continue; + } + work.pop_back(); + if (!work.empty()) { + auto& parent = state.lowlink[work.back().blockId]; + parent = std::min(parent, state.lowlink[blockId]); + } + if (state.lowlink[blockId] == state.index[blockId]) { + emitComponent(state, blockId); + } + } +} + bool forcedFlagStructurization() { static const bool forced = std::getenv("APS5_FORCE_STRUCTURIZER_FALLBACK") != nullptr; return forced; @@ -2143,6 +2291,9 @@ void Structurizer::structurize(ControlFlowGraph& graph, bool privatizeReturns, b void Structurizer::computeDominatorTree(ControlFlowGraph& graph) const { const auto count = static_cast(graph.blocks.size()); + if (inPlaceIntersections() && dominatorTrees() && graph.entryBlock < count && assignTreeDominators(graph.blocks, {graph.entryBlock}, [&](std::uint32_t id) -> const std::vector& { return graph.blocks[id].predecessors; }, &BasicBlock::dominators)) { + return; + } const auto order = analysisOrder(graph, false); if (inPlaceIntersections()) { std::vector sets; @@ -2206,7 +2357,7 @@ void Structurizer::detectNaturalLoops(ControlFlowGraph& graph) const { for (const auto blockId : loop.blocks) { const auto& block = graph.FindBlock(blockId); for (const auto successor : block.successors) { - if (!contains(loop.blocks, successor)) { + if (!containsSorted(loop.blocks, successor)) { addUnique(loop.exitBlocks, successor); } } @@ -2227,6 +2378,16 @@ void Structurizer::detectNaturalLoops(ControlFlowGraph& graph) const { void Structurizer::computePostDominators(ControlFlowGraph& graph) const { const auto count = static_cast(graph.blocks.size()); + if (inPlaceIntersections() && dominatorTrees()) { + std::vector exits; + for (const auto& block : graph.blocks) { + if (block.successors.empty()) exits.push_back(block.id); + } + if (assignTreeDominators(graph.blocks, exits, [&](std::uint32_t id) -> const std::vector& { return graph.blocks[id].successors; }, &BasicBlock::postDominators, &BasicBlock::immediatePostDominator)) { + return; + } + } + for (auto& block : graph.blocks) block.immediatePostDominator = InvalidControlFlowId; const auto order = analysisOrder(graph, true); if (inPlaceIntersections()) { std::vector sets; @@ -2470,13 +2631,21 @@ void Structurizer::verifyReducibility(const ControlFlowGraph& graph) const { } bool Structurizer::hasStructuredConstructs(const ControlFlowGraph& graph) const { + std::vector loopHeaders; + for (const auto& block : graph.blocks) { + if (block.terminator.loopHeader) loopHeaders.push_back(&block); + } + const auto innermostLoop = [&](const ControlFlowGraph& analysis, std::uint32_t blockId) { + const BasicBlock* innermost = nullptr; + for (const auto* header : loopHeaders) { + if (analysis.Dominates(header->id, blockId) && !analysis.Dominates(header->terminator.mergeBlock, blockId) && (innermost == nullptr || analysis.Dominates(innermost->id, header->id))) innermost = header; + } + return innermost; + }; for (const auto& block : graph.blocks) { const auto& terminator = block.terminator; if (terminator.kind != TerminatorKind::ConditionalBranch || terminator.loopHeader || terminator.mergeBlock != InvalidControlFlowId || terminator.trueBlock == terminator.falseBlock) continue; - const BasicBlock* loop = nullptr; - for (const auto& header : graph.blocks) { - if (header.terminator.loopHeader && graph.Dominates(header.id, block.id) && !graph.Dominates(header.terminator.mergeBlock, block.id) && (loop == nullptr || graph.Dominates(loop->id, header.id))) loop = &header; - } + const auto* loop = innermostLoop(graph, block.id); if (loop == nullptr || (terminator.trueBlock != loop->terminator.mergeBlock && terminator.trueBlock != loop->terminator.continueBlock && terminator.falseBlock != loop->terminator.mergeBlock && terminator.falseBlock != loop->terminator.continueBlock)) return false; } auto structural = graph; @@ -2500,18 +2669,10 @@ bool Structurizer::hasStructuredConstructs(const ControlFlowGraph& graph) const const auto inConstruct = [&](const BasicBlock& header, std::uint32_t blockId) { return inRegion(header, blockId) && !(header.terminator.loopHeader && inContinue(header, blockId)); }; - const auto innermostLoop = [&](std::uint32_t blockId) { - const BasicBlock* innermost = nullptr; - for (const auto& header : graph.blocks) { - if (header.terminator.loopHeader && inRegion(header, blockId) && (innermost == nullptr || structural.Dominates(innermost->id, header.id))) { - innermost = &header; - } - } - return innermost; - }; const auto exitsLoop = [](const BasicBlock* loop, std::uint32_t target) { return loop != nullptr && (target == loop->terminator.mergeBlock || target == loop->terminator.continueBlock); }; + const bool walkConstructs = constructsFollowDominance(structural); for (const auto& block : graph.blocks) { const auto& terminator = block.terminator; for (const auto successor : block.successors) { @@ -2521,7 +2682,7 @@ bool Structurizer::hasStructuredConstructs(const ControlFlowGraph& graph) const } } if (terminator.kind == TerminatorKind::ConditionalBranch && !terminator.loopHeader && terminator.mergeBlock == InvalidControlFlowId && terminator.trueBlock != terminator.falseBlock) { - const auto* loop = innermostLoop(block.id); + const auto* loop = innermostLoop(structural, block.id); if (!exitsLoop(loop, terminator.trueBlock) && !exitsLoop(loop, terminator.falseBlock)) { return false; } @@ -2535,8 +2696,8 @@ bool Structurizer::hasStructuredConstructs(const ControlFlowGraph& graph) const if (terminator.loopHeader && (terminator.continueBlock == terminator.mergeBlock || !structural.Dominates(block.id, terminator.continueBlock))) { return false; } - const auto* loop = terminator.loopHeader ? &block : innermostLoop(block.id); - for (const auto& member : graph.blocks) { + const auto* loop = terminator.loopHeader ? &block : innermostLoop(structural, block.id); + const auto memberFits = [&](const BasicBlock& member) { if (inConstruct(block, member.id)) { if (member.id != block.id && member.terminator.mergeBlock != InvalidControlFlowId && !inConstruct(block, member.terminator.mergeBlock)) { return false; @@ -2552,6 +2713,20 @@ bool Structurizer::hasStructuredConstructs(const ControlFlowGraph& graph) const return false; } } + return true; + }; + if (walkConstructs) { + auto members = dominatedBlocks(structural, block.id, terminator.mergeBlock); + if (terminator.loopHeader) { + const auto continueMembers = dominatedBlocks(structural, terminator.continueBlock); + members.insert(members.end(), continueMembers.begin(), continueMembers.end()); + sortUnique(members); + } + if (!std::all_of(members.begin(), members.end(), [&](std::uint32_t memberId) { return memberFits(graph.FindBlock(memberId)); })) { + return false; + } + } else if (!std::all_of(graph.blocks.begin(), graph.blocks.end(), memberFits)) { + return false; } if (terminator.loopHeader) { const auto& predecessors = graph.FindBlock(terminator.continueBlock).predecessors; diff --git a/core/shader/recompiler/FragmentOutputSpecialization.hpp b/core/shader/recompiler/FragmentOutputSpecialization.hpp index e3360cceaf..515c9d635f 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::Unorm4_4_4_4)) throw std::runtime_error("invalid prepared fragment export packing"); + if (constant.value > static_cast(ColorExportPacking::Uint)) 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/src/IrValue.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp index a7cc1db50d..6d1e69deb7 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp @@ -39,9 +39,11 @@ void IrValue::ReplaceArgument(std::size_t index, IrValue* argument) { } IrValue* previous = arguments[index]; if (previous != nullptr) { - const auto operandIt = std::find_if(previous->operandUses.begin(), previous->operandUses.end(), [this, index](const IrUse& use) { - return use.user == this && use.operand == index; - }); + const auto operandIt = !previous->operandUses.empty() && previous->operandUses.back() == IrUse{this, index} + ? previous->operandUses.end() - 1 + : std::find_if(previous->operandUses.begin(), previous->operandUses.end(), [this, index](const IrUse& use) { + return use.user == this && use.operand == index; + }); if (operandIt != previous->operandUses.end()) { previous->operandUses.erase(operandIt); } diff --git a/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp index 0849790845..f5a711b969 100644 --- a/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp @@ -101,6 +101,23 @@ void CheckSimpleReplacement() { Require(source->Opcode() == IrOpcode::IAdd32 && source->Parent() == &graph.block); } +void CheckTailArgumentRemoval() { + Graph graph(4); + auto* source = graph.Value(0); + auto* first = graph.Value(1); + auto* last = graph.Value(2); + first->AddArgument(source); + last->AddArgument(source); + last->AddArgument(source); + last->ReplaceArgument(0, nullptr); + Require(source->OperandUses() == std::vector({{first, 0}, {last, 1}})); + last->ReplaceArgument(1, graph.Value(3)); + Require(source->OperandUses() == std::vector({{first, 0}})); + Require(graph.Value(3)->OperandUses() == std::vector({{last, 1}})); + first->Invalidate(); + Require(!source->HasUses() && first->Argument(0) == nullptr); +} + void CheckSelfAndEmpty() { Graph graph(3); graph.Value(2)->AddArgument(graph.Value(0)); @@ -223,6 +240,7 @@ int main() { try { const std::pair cases[] = { {"ordered duplicate operands", CheckSimpleReplacement}, + {"tail argument removal", CheckTailArgumentRemoval}, {"self and empty replacement", CheckSelfAndEmpty}, {"phi and identity rewriting", CheckPhiAndIdentity}, {"allocation failure integrity", CheckAllocationFailure}, @@ -234,7 +252,7 @@ int main() { current = name; test(); } - std::puts("7 IR rewrite suites passed; 2000 differential use graphs and 48000 mutation comparisons"); + std::puts("8 IR rewrite suites passed; 2000 differential use graphs and 48000 mutation comparisons"); return 0; } catch (const std::exception& error) { failureAfter = -1; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp index ae3696883c..ad1a0f01b5 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp @@ -21,6 +21,7 @@ struct ImageTableElement { DescriptorBindingKind kind = DescriptorBindingKind::Buffers; std::uint32_t samplers = 0; bool atomic = false; + bool atomic64 = false; }; struct ImageTableSamplerElement { diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index f11023d4b4..576ac75a42 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -344,7 +344,6 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const auto first = PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords; plan.specialization.push_back({first, descriptor.dwords[1] & 0xffff0000u}); plan.specialization.push_back({first + 1u, descriptor.dwords[3]}); - plan.specialization.push_back({first + 2u, present ? 1u : 0u}); plan.specialization.push_back({first + 3u, misalignment}); } if (buffer.formattedReadMask != 0u) { @@ -616,6 +615,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con physical.imageDepthCompare.push_back(element.depthCompare); 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; for (std::uint32_t sampler = 0; sampler < entry.tableSamplerMasks.size(); ++sampler) { if ((element.samplers & (1u << sampler)) != 0u) samplers |= entry.tableSamplerMasks[sampler]; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 921b9bb653..10202666a4 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -262,11 +262,11 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag : "MIMG BY2/BY4 requires a direct, identity-swizzled 2D image whose elements fill one dword: R8, R16 or RG8 for BY2, R8 for BY4"); } } - const bool nativeRepacked = format == IrBufferFormat::Format10_11_11Float && !storage && !base.packed; + const bool nativeRepacked = (format == IrBufferFormat::Format10_11_11Float || format == IrBufferFormat::Format11_11_10Float) && !storage && !base.packed; const auto remappedFormat = storage ? RemapStorageTextureFormat(format) : RemapTextureFormat(format); decoded.conversionFormat = remappedFormat != format && !nativeRepacked ? format : IrBufferFormat::Invalid; - if (format == IrBufferFormat::Format11_11_10UNorm || (format == IrBufferFormat::Format10_11_11Float && !nativeRepacked)) { - const bool floating = format == IrBufferFormat::Format10_11_11Float; + if (format == IrBufferFormat::Format11_11_10UNorm || ((format == IrBufferFormat::Format10_11_11Float || format == IrBufferFormat::Format11_11_10Float) && !nativeRepacked)) { + const bool floating = format == IrBufferFormat::Format10_11_11Float || format == IrBufferFormat::Format11_11_10Float; if (!base.srgbDecodeCompatible) { if (floating) return rejectImage(PoisonReason::Conversion, "samples or gathers a converted float image, or queries its LOD, which is not implemented"); if (storage || base.depthCompare || (base.emulatedCompare & EmulatedCompare::ConvertedSampleUnsupported) != 0u) return rejectImage(PoisonReason::Conversion, "sampling, gathering or querying LOD of a converted unorm image is not implemented"); @@ -1396,7 +1396,7 @@ void materializeForcedDegamma(const IrResourcePlan& plan, ResourceSnapshot& snap std::uint32_t colorCompareReference(IrBufferFormat format) { const auto value = static_cast(format); - if (((value >= 1u && value <= 77u) || (value >= 128u && value <= 130u)) && SampledTextureNumericClass(format) == IrTextureNumericClass::Float && format != IrBufferFormat::Format11_11_10UNorm && format != IrBufferFormat::Format10_11_11Float) return EmulatedCompare::ReferenceZero; + if (((value >= 1u && value <= 77u) || (value >= 128u && value <= 130u)) && (SampledTextureNumericClass(format) == IrTextureNumericClass::Float || format == IrBufferFormat::Format11_11_10Float) && format != IrBufferFormat::Format11_11_10UNorm && format != IrBufferFormat::Format10_11_11Float) return EmulatedCompare::ReferenceZero; throw std::runtime_error("comparison sampling of a color texture is implemented only for floating-point, UNORM and SNORM formats (format " + std::to_string(value) + ")"); } @@ -1716,17 +1716,17 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i auto element = words.dwords; if (mode.conversionFormat != IrBufferFormat::Invalid || mode.depthBits) element[3] = (element[3] & ~0xfffu) | ShaderImageIdentitySwizzle; const auto kind = DescriptorBindingForImage(mode); - const auto hash = hashWords(element, (static_cast(image.atomic) << 24u) ^ (static_cast(mode.dimension) << 16u) ^ (static_cast(mode.depthCompare) << 8u) ^ static_cast(kind)); + const auto hash = hashWords(element, (static_cast(image.atomic64) << 25u) ^ (static_cast(image.atomic) << 24u) ^ (static_cast(mode.dimension) << 16u) ^ (static_cast(mode.depthCompare) << 8u) ^ static_cast(kind)); auto index = elementIndex.Find(hash, [&](std::uint32_t known) { const auto& entry = out.elements[known]; - return entry.words == element && entry.dimension == mode.dimension && entry.depthCompare == mode.depthCompare && entry.kind == kind && entry.atomic == image.atomic; + return entry.words == element && entry.dimension == mode.dimension && entry.depthCompare == mode.depthCompare && entry.kind == kind && entry.atomic == image.atomic && entry.atomic64 == image.atomic64; }); if (!index.has_value() && out.elements.size() >= Abi::ImageElementBudget) { record[Abi::ImageRecordFlags] = poison(resource, PoisonReason::Budget, &words); } else { if (!index.has_value()) { index = static_cast(out.elements.size()); - out.elements.push_back({element, mode.dimension, mode.depthCompare, kind, 0u, image.atomic}); + out.elements.push_back({element, mode.dimension, mode.depthCompare, kind, 0u, image.atomic, image.atomic64}); elementIndex.Insert(hash, *index); } out.elements[*index].samplers |= directSamplers[resource]; @@ -1895,7 +1895,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe mode.cube = false; mode.mipCount = mode.mipMode == ImageMipMode::DynamicStorage ? RuntimeAbi::StorageMipSlots : 1u; mode.shaderSwizzle = ShaderImageIdentitySwizzle; - if (conversion == IrBufferFormat::Format11_11_10UNorm || conversion == IrBufferFormat::Format10_11_11Float) mode.shaderSwizzle = 0x2acu; + if (conversion == IrBufferFormat::Format11_11_10UNorm || conversion == IrBufferFormat::Format11_11_10Float || conversion == IrBufferFormat::Format10_11_11Float) mode.shaderSwizzle = 0x2acu; modes.push_back(mode); const bool volumeLoad = (image.dimension == RdnaImageDimension::Dim2D || image.dimension == RdnaImageDimension::Dim2DArray) && image.fmaskCompatible && conversion != FmaskExpansion; const bool volumeStore = image.dimension == RdnaImageDimension::Dim2DArray && storage && image.written && !image.read && !image.atomic && image.mipMode != ImageMipMode::DynamicStorage; @@ -1929,7 +1929,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe line.dimension = RdnaImageDimension::Dim1D; modes.push_back(line); } - if (image.dimension == RdnaImageDimension::Dim2D && storage && image.written && !image.read && !image.atomic && image.mipMode != ImageMipMode::DynamicStorage && !depth && packed == IrBufferFormat::Invalid && image.byElements == 0u) { + if ((image.dimension == RdnaImageDimension::Dim2D || image.dimension == RdnaImageDimension::Dim2DArray) && storage && image.written && !image.read && !image.atomic && image.mipMode != ImageMipMode::DynamicStorage && !depth && packed == IrBufferFormat::Invalid && image.byElements == 0u) { auto line = mode; line.dimension = RdnaImageDimension::Dim1D; modes.push_back(line); @@ -1999,7 +1999,10 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe if (!storage) append(IrTextureNumericClass::Sint, IrBufferFormat::Invalid, IrBufferFormat::Invalid, false, false); append(IrTextureNumericClass::Uint, IrBufferFormat::Format11_11_10UInt, IrBufferFormat::Invalid, false, false); if ((image.srgbDecodeCompatible || (image.convertedSamples && convertedSampleCompatible(image))) && image.depthBitsCompatible) append(IrTextureNumericClass::Uint, IrBufferFormat::Format11_11_10UNorm, IrBufferFormat::Invalid, false, false); - if (image.srgbDecodeCompatible && image.depthBitsCompatible) append(IrTextureNumericClass::Uint, IrBufferFormat::Format10_11_11Float, IrBufferFormat::Invalid, false, false); + if (image.srgbDecodeCompatible && image.depthBitsCompatible) { + append(IrTextureNumericClass::Uint, IrBufferFormat::Format11_11_10Float, IrBufferFormat::Invalid, false, false); + append(IrTextureNumericClass::Uint, IrBufferFormat::Format10_11_11Float, IrBufferFormat::Invalid, false, false); + } if (storage) { for (const auto format : {IrBufferFormat::Format5_6_5UNorm, IrBufferFormat::Format5_5_5_1UNorm, IrBufferFormat::Format4_4_4_4UNorm}) { append(IrTextureNumericClass::Uint, format, IrBufferFormat::Invalid, false, false); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index ee6873ef41..d0d0482387 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -1775,7 +1775,7 @@ private: continue; } image.table = next++; - if (image.atomic64 || image.packed || image.byElements != 0u) { + if (image.packed || image.byElements != 0u) { image.tableOperation = TableOperation::Unsupported; } } diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp index acdbad34d0..f84540b2f0 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp @@ -38,7 +38,7 @@ constexpr std::array kFormatInfoTable {{ {IrBufferFormat::Format16_16Float, true, false, false}, {IrBufferFormat::Format11_11_10UNorm, true, true, false}, {IrBufferFormat::Format11_11_10UInt, true, true, false}, - {IrBufferFormat::Format11_11_10Float, true, false, false}, + {IrBufferFormat::Format11_11_10Float, true, true, false}, {IrBufferFormat::Format10_11_11Float, true, true, false}, {IrBufferFormat::Format10_10_10_2UNorm, true, false, false}, {IrBufferFormat::Format10_10_10_2SNorm, true, false, false}, @@ -139,7 +139,7 @@ IrTextureNumericClass SampledTextureNumericClass(IrBufferFormat format) { } IrBufferFormat RemapTextureFormat(IrBufferFormat format) { - return format == IrBufferFormat::Format11_11_10UNorm || format == IrBufferFormat::Format11_11_10UInt || format == IrBufferFormat::Format10_11_11Float ? IrBufferFormat::Format32UInt : format; + return format == IrBufferFormat::Format11_11_10UNorm || format == IrBufferFormat::Format11_11_10UInt || format == IrBufferFormat::Format11_11_10Float || format == IrBufferFormat::Format10_11_11Float ? IrBufferFormat::Format32UInt : format; } IrBufferFormat RemapStorageTextureFormat(IrBufferFormat format) { diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp index e3be618395..e5a71be9c7 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp @@ -86,6 +86,8 @@ constexpr ImageOpcodeInfo imageOpcodes[] = { {0x4fu, RdnaOpcode::ImageGather4CLz, nullptr, RdnaImageSampleFlagCompare | RdnaImageSampleFlagLevelZero, false, true, false}, {0x57u, RdnaOpcode::ImageGather4LzO, nullptr, RdnaImageSampleFlagLevelZero | RdnaImageSampleFlagOffset, false, true, false}, {0x58u, RdnaOpcode::ImageGather4CO, nullptr, RdnaImageSampleFlagCompare | RdnaImageSampleFlagOffset, false, true, false}, + {0xc8u, RdnaOpcode::ImageGather4C, "image_gather4_c_a", RdnaImageSampleFlagCompare | RdnaImageSampleFlagAdjust, false, true, false}, + {0xd8u, RdnaOpcode::ImageGather4CO, "image_gather4_c_o_a", RdnaImageSampleFlagCompare | RdnaImageSampleFlagOffset | RdnaImageSampleFlagAdjust, false, true, false}, {0x5fu, RdnaOpcode::ImageGather4CLzO, nullptr, RdnaImageSampleFlagCompare | RdnaImageSampleFlagLevelZero | RdnaImageSampleFlagOffset, false, true, false}, {0x61u, RdnaOpcode::ImageGather4h, nullptr, RdnaImageSampleFlagGatherHorizontal, false, true, false}, {0x62u, RdnaOpcode::ImageGather4hPck, nullptr, RdnaImageSampleFlagGatherHorizontal, false, true, false}, diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp index 02ccb674b8..d3c9624463 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -2480,9 +2480,6 @@ RdnaInstruction DecodeRdnaVop3p(std::uint32_t programCounter, std::span 2u) { instruction.source2 = DecodeRdnaScalarSource(src2, programCounter); } - if (instruction.op == RdnaOpcode::VDot2F32F16 && ((((opSel | negHi) >> 2u) & 1u) != 0u || ((opSelHi >> 2u) & 1u) == 0u)) { - throw std::invalid_argument("VOP3P v_dot2_f32_f16 accumulator op_sel, op_sel_hi and neg_hi are not implemented"); - } if (instruction.op == RdnaOpcode::VFmaF32 || instruction.op == RdnaOpcode::VDot2F32F16 || isMadMixF16(instruction.op) || isPackedVop3p(instruction.op) || isVop3pIntegerSaturate(instruction.op)) { instruction.destination.clamp = clamp != 0u; } else if (clamp != 0u && instruction.op != RdnaOpcode::VPkMulLoU16) { @@ -2493,6 +2490,12 @@ RdnaInstruction DecodeRdnaVop3p(std::uint32_t programCounter, std::span>> buffers; + std::vector userWords; + std::uint32_t shaderData = NoBinding; + std::uint32_t flattenedSrt = NoBinding; + bool pushData = false; + bool valid = false; +}; + struct ResultMemoEntry { std::uint64_t variantId = 0; std::uint64_t hash = 0; std::shared_ptr result; std::uint64_t used = 0; + std::shared_ptr patch; + std::uint64_t bytes = 0; }; struct EmissionFailure { @@ -387,7 +399,7 @@ struct SourceEntry : std::enable_shared_from_this { std::vector> variants; std::vector emissionFailures; std::unique_ptr memo; - std::uint64_t memoClock = 0; + std::unique_ptr maskedMemo; }; namespace { @@ -743,12 +755,19 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra struct SpecializedModule { std::uint64_t specializationId = 0; - SharedSpirv spirv; std::vector bindings; bool pushData = false; + SpirvContentHash hash; + mutable std::mutex mutex; + mutable SharedSpirv::Weak spirv; }; -std::shared_ptr buildSpecializedModule(const CompiledShaderArtifact& artifact, std::span classes, std::span constants, const SpirvTarget& target) { +struct SpecializedSpirv { + std::shared_ptr module; + SharedSpirv spirv; +}; + +std::vector specializeWords(const CompiledShaderArtifact& artifact, std::span classes, std::span constants, const SpirvTarget& target) { auto source = SpecializeVertexInputTypes(artifact, classes); std::map supplied; for (const auto& constant : constants) { @@ -842,6 +861,11 @@ std::shared_ptr buildSpecializedModule(const CompiledSh #if ANYPS5_ENABLE_SPIRV_TOOLS materialized = ValidateAndOptimizeSpirv(materialized, target.vulkanVersion, target.spirvVersion, target.nonConstantImageOffsets, false); #endif + return materialized; +} + +SpecializedSpirv buildSpecializedModule(const CompiledShaderArtifact& artifact, std::span classes, std::span constants, const SpirvTarget& target) { + auto materialized = specializeWords(artifact, classes, constants, target); auto module = std::make_shared(); std::map bindingNumbers; std::unordered_set functionLdsBuffer; @@ -856,9 +880,20 @@ std::shared_ptr buildSpecializedModule(const CompiledSh } cursor += count; } - module->spirv = InternSpirv(std::move(materialized)); + auto spirv = InternSpirv(std::move(materialized)); + module->hash = spirv.ContentHash(); + module->spirv = spirv.Weaken(); module->specializationId = constants.empty() ? 0u : nextVariantId(); - return module; + return {std::move(module), std::move(spirv)}; +} + +SharedSpirv specializedWords(const SpecializedModule& module, const CompiledShaderArtifact& artifact, std::span classes, std::span constants, const SpirvTarget& target) { + std::lock_guard lock(module.mutex); + if (auto spirv = module.spirv.Lock(); !spirv.empty()) return spirv; + auto spirv = InternSpirv(specializeWords(artifact, classes, constants, target)); + if (spirv.ContentHash() != module.hash) throw std::runtime_error("ShaderRecompiler: a specialized module built again from its key differs from its first build"); + module.spirv = spirv.Weaken(); + return spirv; } struct SpecializedModuleEntry { @@ -888,7 +923,12 @@ struct RegistrationModuleEntry { std::shared_ptr module; }; -std::shared_ptr registrationModule(std::uint64_t variantId, std::span moduleKey, const std::function()>& specialize) { +SharedSpirv lockedWords(const SpecializedModule& module) { + std::lock_guard lock(module.mutex); + return module.spirv.Lock(); +} + +SpecializedSpirv registrationModule(std::uint64_t variantId, std::span moduleKey, const std::function& specialize) { std::vector key{variantId}; key.insert(key.end(), moduleKey.begin(), moduleKey.end()); static PrimedSharedMutex mutex; @@ -903,15 +943,20 @@ std::shared_ptr registrationModule(std::uint64_t varian const auto found = modules.find(key); entry = found != modules.end() ? found->second : modules.emplace(key, std::make_shared()).first->second; } + SharedSpirv built; std::call_once(entry->ready, [&] { try { - entry->module = specialize(); + auto specialized = specialize(); + entry->module = std::move(specialized.module); + built = std::move(specialized.spirv); } catch (...) { entry->failure = std::current_exception(); } }); if (entry->failure) std::rethrow_exception(entry->failure); - return entry->module; + if (built.empty()) built = lockedWords(*entry->module); + if (built.empty()) built = specialize().spirv; + return {entry->module, std::move(built)}; } std::shared_ptr preparedBindingPlan(const CompiledVariant& variant, const ResourceSnapshot& snapshot, std::span exports, bool fragmentDescriptorSet) { @@ -964,7 +1009,7 @@ std::shared_ptr preparedBindingPlan(const CompiledVariant& return plan; } -std::shared_ptr specializeModule(const CompiledShaderArtifact& artifact, std::span classes, std::span constants, const SpirvTarget& target) { +SpecializedSpirv specializeModule(const CompiledShaderArtifact& artifact, std::span classes, std::span constants, const SpirvTarget& target) { struct ModuleKeyStorage {}; auto& key = HostThreadLocal, ModuleKeyStorage>(); key.clear(); @@ -982,15 +1027,19 @@ std::shared_ptr specializeModule(const CompiledShaderAr const auto found = modules.find(key); entry = found != modules.end() ? found->second : modules.emplace(key, std::make_shared()).first->second; } + SharedSpirv built; std::call_once(entry->ready, [&] { try { - entry->module = buildSpecializedModule(artifact, classes, constants, target); + auto specialized = buildSpecializedModule(artifact, classes, constants, target); + entry->module = std::move(specialized.module); + built = std::move(specialized.spirv); } catch (...) { entry->failure = std::current_exception(); } }); if (entry->failure) std::rethrow_exception(entry->failure); - return entry->module; + if (built.empty()) built = specializedWords(*entry->module, artifact, classes, constants, target); + return {entry->module, std::move(built)}; } const ResourceSnapshot& mergeTargetImage(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot, bool registrationDefaults, ResourceSnapshot& merged) { @@ -1008,7 +1057,28 @@ const ResourceSnapshot& mergeTargetImage(const CompiledVariant& variant, const R return merged; } -RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& captured, bool registrationDefaults = false) { +void recordMemoPatch(MemoPatch& patch, const DescriptorBindingPlan& plan, const CompiledVariant& variant) { + const auto& layout = variant.bindings.layout; + patch = {}; + for (std::size_t index = 0; index < plan.bindings.size(); ++index) { + const auto& binding = plan.bindings[index]; + const auto position = static_cast(index); + if (binding.descriptor.role == DescriptorRole::GuestBuffers) { + patch.buffers.push_back({position, binding.resources}); + } else if (binding.descriptor.role == DescriptorRole::FlattenedSrt) { + patch.flattenedSrt = position; + } else if (binding.descriptor.role == DescriptorRole::ShaderData) { + patch.shaderData = position; + } + } + patch.pushData = layout.UsesPushData(); + if (patch.pushData || plan.storageData) { + for (const auto reg : layout.userDataRegisters) patch.userWords.push_back(reg - variant.info.userDataBase); + } + patch.valid = layout.userDataRegisters.size() <= layout.DispatchThreadLimitDword() && (patch.userWords.empty() || patch.pushData || patch.shaderData != MemoPatch::NoBinding); +} + +RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& captured, bool registrationDefaults = false, MemoPatch* patch = nullptr) { ResourceSnapshot merged; const auto& snapshot = mergeTargetImage(variant, request, captured, registrationDefaults, merged); RecompileResult result; @@ -1112,7 +1182,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil const auto found = plan->modules.find(moduleKey); entry = found != plan->modules.end() ? found->second : plan->modules.emplace(moduleKey, std::make_shared()).first->second; } - const auto prepare = [&] { + const auto specialize = [&] { auto constants = plan->bindings.specialization; std::size_t index = 0; if (variant.bindings.layout.UsesPushData()) constants.push_back({PipelineSpecialization::PushDataOffset, moduleKey[index++]}); @@ -1137,11 +1207,17 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil constants.push_back({first + 5u, kind}); } } - const auto module = artifact.registrationModule && plan->bindings.specialization.empty() ? registrationModule(artifact.variantId, moduleKey, [&] { return specializeModule(variant.artifact, vertexClasses, constants, request.target); }) : specializeModule(variant.artifact, vertexClasses, constants, request.target); + return artifact.registrationModule && plan->bindings.specialization.empty() ? registrationModule(artifact.variantId, moduleKey, [&] { return specializeModule(variant.artifact, vertexClasses, constants, request.target); }) : specializeModule(variant.artifact, vertexClasses, constants, request.target); + }; + SharedSpirv built; + const auto prepare = [&] { + auto specialized = specialize(); + const auto& module = specialized.module; if (variant.bindings.layout.UsesPushData() && !module->pushData) throw std::runtime_error("specialization removed the prepared push constant interface"); auto selected = DescriptorBindingBuilder{}.Select(plan->bindings, module->bindings); entry->bindings = std::move(selected); entry->module = module; + built = std::move(specialized.spirv); }; std::call_once(entry->ready, [&] { try { @@ -1152,8 +1228,10 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil }); if (entry->failure) std::rethrow_exception(entry->failure); const auto& module = entry->module; + if (built.empty()) built = lockedWords(*module); + if (built.empty()) built = specialize().spirv; result.specializationId = module->specializationId; - result.spirv = module->spirv; + result.spirv = std::move(built); bindingPlan = &entry->bindings; } else { result.spirv = artifact.spirv; @@ -1162,6 +1240,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil const auto& tables = *resolved; BindingAllocationResult bindings; DescriptorBindingBuilder{}.Populate(bindings, variant.bindings, *bindingPlan, variant.info.userDataBase, snapshot, partialThreads(request), &tables); + if (patch != nullptr) recordMemoPatch(*patch, *bindingPlan, variant); result.workgroupMemoryDwords = WorkgroupMemoryStrideDwords(variant.info.info); result.bindings = std::move(bindings.bindings); if (std::ranges::none_of(result.bindings, [](const DescriptorBinding& binding) { return binding.role == DescriptorRole::BdaPagetable || binding.role == DescriptorRole::FaultBuffer; })) result.bdaAbiVersion = 0u; @@ -1211,13 +1290,16 @@ SharedSpirv InternSpirv(std::vector words) { SharedSpirv spirv(std::move(words)); const auto hash = spirv.ContentHash(); static std::mutex mutex; - static std::unordered_map> interned; + static std::unordered_map> interned; std::lock_guard lock(mutex); - auto& candidates = interned[hash.low ^ (hash.high * 0x9e3779b97f4a7c15ull)]; - for (const auto& candidate : candidates) { + const auto bucket = hash.low ^ (hash.high * 0x9e3779b97f4a7c15ull); + auto& candidates = interned[bucket]; + std::erase_if(candidates, [](const SharedSpirv::Weak& candidate) { return candidate.Expired(); }); + for (const auto& weak : candidates) { + const auto candidate = weak.Lock(); if (candidate.ContentHash() == hash && candidate == spirv) return candidate; } - candidates.push_back(spirv); + candidates.push_back(spirv.Weaken()); return spirv; } @@ -1357,21 +1439,196 @@ bool ResultMemo() { return resultMemo; } -ResultMemoEntry* memoSet(SourceEntry& source, std::uint64_t index) { - if (source.memo == nullptr) return nullptr; - return source.memo->data() + static_cast((index ^ (index >> 32u)) % ResultMemoSets) * ResultMemoWays; +bool MaskedResultMemo() { + static const bool masked = std::getenv("APS5_NO_MASKED_RESULT_MEMO") == nullptr; + return masked; +} + +bool VerifyMaskedResultMemo() { + static const bool verify = std::getenv("APS5_VERIFY_MASKED_RESULT_MEMO") != nullptr; + return verify; +} + +ResultMemoEntry* memoSet(const std::unique_ptr& table, std::uint64_t index) { + if (table == nullptr) return nullptr; + return table->data() + static_cast((index ^ (index >> 32u)) % ResultMemoSets) * ResultMemoWays; } struct ResultMemoCounters { - std::atomic hits{0}, misses{0}, evictions{0}, populateNanoseconds{0}; + std::atomic hits{0}, patched{0}, misses{0}, evictions{0}, trimmed{0}, populateNanoseconds{0}, patchNanoseconds{0}, mismatches{0}; std::atomic lastReport{0}; + std::atomic clock{0}, heldBytes{0}, heldEntries{0}; }; +void applyMemoPatch(RecompileResult& result, const MemoPatch& patch, const ResourceSnapshot& snapshot) { + for (const auto& [binding, resources] : patch.buffers) { + auto& words = result.bindings.at(binding).guestDescriptor; + if (words.size() != resources.size() * 4u) throw std::runtime_error("a patched guest buffer binding changed its width"); + for (std::size_t element = 0; element < resources.size(); ++element) std::copy_n(snapshot.buffers.at(resources[element]).dwords.begin(), 4u, words.begin() + element * 4u); + } + if (!patch.userWords.empty()) { + if (patch.pushData) { + if (patch.userWords.size() * sizeof(std::uint32_t) > result.pushConstants.size()) throw std::runtime_error("patched user data lies outside the push data"); + for (std::size_t word = 0; word < patch.userWords.size(); ++word) std::memcpy(result.pushConstants.data() + word * sizeof(std::uint32_t), &snapshot.userData.at(patch.userWords[word]), sizeof(std::uint32_t)); + } else { + auto& words = result.bindings.at(patch.shaderData).guestDescriptor; + if (patch.userWords.size() > words.size()) throw std::runtime_error("patched user data lies outside the shader data"); + for (std::size_t word = 0; word < patch.userWords.size(); ++word) words[word] = snapshot.userData.at(patch.userWords[word]); + } + } + if (patch.flattenedSrt != MemoPatch::NoBinding) result.bindings.at(patch.flattenedSrt).guestDescriptor = snapshot.flattenedSrt; + result.poisonedSrtReads = static_cast(snapshot.srtPoison.size()) + snapshot.nullRootReads; +} + +const char* patchedResultDifference(const RecompileResult& patched, const RecompileResult& full) { + if (patched.bindings.size() != full.bindings.size()) return "binding count"; + if (patched.pushConstants != full.pushConstants) return "push constants"; + if (patched.specializationId != full.specializationId || patched.spirv.ContentHash() != full.spirv.ContentHash()) return "module"; + if (patched.poisonedSrtReads != full.poisonedSrtReads) return "poisoned SRT reads"; + if (patched.shaderDataUserWords != full.shaderDataUserWords) return "shader data user words"; + if (patched.specialization.size() != full.specialization.size()) return "specialization"; + for (std::size_t index = 0; index < patched.specialization.size(); ++index) { + if (patched.specialization[index].id != full.specialization[index].id || patched.specialization[index].value != full.specialization[index].value) return "specialization"; + } + for (std::size_t index = 0; index < patched.bindings.size(); ++index) { + const auto& left = patched.bindings[index]; + const auto& right = full.bindings[index]; + if (left.role != right.role || left.binding != right.binding || left.count != right.count) return "binding shape"; + if (left.guestDescriptor != right.guestDescriptor) return "binding words"; + } + return nullptr; +} + ResultMemoCounters& resultMemoCounters() { static ResultMemoCounters counters; return counters; } +std::uint64_t nextMemoUse() { + return resultMemoCounters().clock.fetch_add(1, std::memory_order_relaxed) + 1u; +} + +template +std::uint64_t vectorBytes(const std::vector& values) { + return static_cast(values.capacity()) * sizeof(T); +} + +std::uint64_t vectorBytes(const std::vector& values) { + return static_cast(values.capacity() + 7u) / 8u; +} + +std::uint64_t descriptorBindingBytes(const DescriptorBinding& binding) { + return vectorBytes(binding.guestDescriptor) + vectorBytes(binding.imageShapes) + vectorBytes(binding.imageSamplers) + vectorBytes(binding.bufferExtent) + vectorBytes(binding.samplerDepthCompare) + vectorBytes(binding.imageWritten) + vectorBytes(binding.imageDepthCompare) + vectorBytes(binding.imageAtomic) + vectorBytes(binding.imageAtomic64) + vectorBytes(binding.bufferAtomic) + vectorBytes(binding.bufferWritten) + vectorBytes(binding.samplerUnnormalized) + vectorBytes(binding.samplerIntegerBorder) + vectorBytes(binding.samplerCoordinatesIgnored) + vectorBytes(binding.imageUnnormalized) + vectorBytes(binding.imageUnnormalizedBaseLevel) + vectorBytes(binding.bufferRead); +} + +std::uint64_t resultBytes(const RecompileResult& result) { + const auto bytes = [](const auto& values) { return vectorBytes(values); }; + auto total = static_cast(sizeof(RecompileResult)) + bytes(result.vertexInputPatches) + bytes(result.runtimeImageResources) + bytes(result.vertexInputs) + bytes(result.parameterExports) + bytes(result.fragmentParameters) + bytes(result.stencilReferences) + bytes(result.shaderDataUserWords) + bytes(result.specialization) + bytes(result.bindings) + bytes(result.pushConstants) + bytes(result.vertexAttributes) + bytes(result.imageTablePoison) + bytes(result.imageTableRanges) + bytes(result.imageTableReadRanges); + for (const auto& ranges : result.imageTableReadRanges) total += bytes(ranges); + for (const auto& binding : result.bindings) total += descriptorBindingBytes(binding); + return total; +} + +std::uint64_t patchBytes(const MemoPatch& patch) { + auto total = static_cast(sizeof(MemoPatch)) + vectorBytes(patch.buffers) + vectorBytes(patch.userWords); + for (const auto& buffer : patch.buffers) total += vectorBytes(buffer.second); + return total; +} + +std::atomic& resultMemoBudgetBytes() { + static std::atomic budget{[] { + const char* value = std::getenv("APS5_RESULT_MEMO_MB"); + const std::uint64_t megabytes = value == nullptr || *value == '\0' ? 256u : std::strtoull(value, nullptr, 10); + return megabytes << 20u; + }()}; + return budget; +} + +std::uint64_t resultMemoBudget() { + return resultMemoBudgetBytes().load(std::memory_order_relaxed); +} + +struct ResultMemoRegistry { + std::mutex mutex; + std::vector> sources; +}; + +ResultMemoRegistry& resultMemoRegistry() { + static ResultMemoRegistry registry; + return registry; +} + +std::mutex& resultMemoTrimMutex() { + static std::mutex mutex; + return mutex; +} + +std::function& resultMemoTrimHook() { + static std::function hook; + return hook; +} + +void trimResultMemo(std::uint64_t budget) { + std::lock_guard trim(resultMemoTrimMutex()); + auto& counters = resultMemoCounters(); + for (auto held = counters.heldBytes.load(std::memory_order_relaxed); held > budget; held = counters.heldBytes.load(std::memory_order_relaxed)) { + if (auto hook = std::exchange(resultMemoTrimHook(), nullptr)) hook(); + std::vector> sources; + { + auto& registry = resultMemoRegistry(); + std::lock_guard lock(registry.mutex); + sources.reserve(registry.sources.size()); + for (const auto& weak : registry.sources) { + if (auto source = weak.lock()) sources.push_back(std::move(source)); + } + } + std::vector> stamps; + for (const auto& source : sources) { + std::lock_guard lock(source->mutex); + for (const auto* table : {source->memo.get(), source->maskedMemo.get()}) { + if (table == nullptr) continue; + for (const auto& entry : *table) { + if (entry.result != nullptr) stamps.emplace_back(entry.used, entry.bytes); + } + } + } + if (stamps.empty()) return; + std::sort(stamps.begin(), stamps.end()); + const auto excess = held - budget / 4u * 3u; + std::uint64_t freed = 0; + std::uint64_t threshold = 0; + for (const auto& [used, bytes] : stamps) { + threshold = used; + freed += bytes; + if (freed >= excess) break; + } + std::uint64_t dropped = 0; + for (const auto& source : sources) { + std::lock_guard lock(source->mutex); + for (auto* table : {source->memo.get(), source->maskedMemo.get()}) { + if (table == nullptr) continue; + for (auto& entry : *table) { + if (entry.result == nullptr || entry.used > threshold) continue; + counters.heldBytes.fetch_sub(entry.bytes, std::memory_order_relaxed); + counters.heldEntries.fetch_sub(1, std::memory_order_relaxed); + counters.trimmed.fetch_add(1, std::memory_order_relaxed); + entry = {}; + ++dropped; + } + } + } + if (dropped == 0) return; + } +} + +void verifyPatchedResult(const RecompileResult& patched, const RecompileResult& full, std::uint64_t variantId) { + const auto* difference = patchedResultDifference(patched, full); + if (difference == nullptr) return; + resultMemoCounters().mismatches.fetch_add(1, std::memory_order_relaxed); + static std::atomic printed{0}; + if (const auto index = printed.fetch_add(1, std::memory_order_relaxed); index < 20u) std::fprintf(stderr, "[recompile] masked result memo mismatch %u: variant %016llx differs in its %s\n", index + 1u, static_cast(variantId), difference); +} + void reportResultMemo() { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; if (!profile) return; @@ -1384,16 +1641,21 @@ void reportResultMemo() { } if (now - last < 10'000'000'000ll || !counters.lastReport.compare_exchange_strong(last, now, std::memory_order_relaxed)) return; const auto hits = counters.hits.exchange(0, std::memory_order_relaxed); + const auto patched = counters.patched.exchange(0, std::memory_order_relaxed); const auto misses = counters.misses.exchange(0, std::memory_order_relaxed); const auto evictions = counters.evictions.exchange(0, std::memory_order_relaxed); + const auto trimmed = counters.trimmed.exchange(0, std::memory_order_relaxed); const auto populate = counters.populateNanoseconds.exchange(0, std::memory_order_relaxed); - std::fprintf(stderr, "[recompile] result memo (10 s): %llu hits, %llu misses (%.1f%% hits), Populate %.1f us per miss / %.1f ms in total, %llu evictions\n", static_cast(hits), static_cast(misses), hits + misses != 0 ? 100.0 * static_cast(hits) / static_cast(hits + misses) : 0.0, misses != 0 ? static_cast(populate) / 1000.0 / static_cast(misses) : 0.0, static_cast(populate) / 1e6, static_cast(evictions)); + const auto patching = counters.patchNanoseconds.exchange(0, std::memory_order_relaxed); + const auto mismatches = counters.mismatches.exchange(0, std::memory_order_relaxed); + const auto total = static_cast(hits + patched + misses); + std::fprintf(stderr, "[recompile] result memo (10 s): %llu hits, %llu patched, %llu misses (%.1f%% hits, %.1f%% patched), Populate %.1f us per miss / %.1f ms in total, patch %.1f us each / %.1f ms in total, %llu evictions, %llu patch mismatches, %llu trimmed; %llu results held, %.1f MiB\n", static_cast(hits), static_cast(patched), static_cast(misses), total != 0.0 ? 100.0 * static_cast(hits) / total : 0.0, total != 0.0 ? 100.0 * static_cast(patched) / total : 0.0, misses != 0 ? static_cast(populate) / 1000.0 / static_cast(misses) : 0.0, static_cast(populate) / 1e6, patched != 0 ? static_cast(patching) / 1000.0 / static_cast(patched) : 0.0, static_cast(patching) / 1e6, static_cast(evictions), static_cast(mismatches), static_cast(trimmed), static_cast(counters.heldEntries.load(std::memory_order_relaxed)), static_cast(counters.heldBytes.load(std::memory_order_relaxed)) / 1048576.0); } // Everything materializeResult reads besides the variant: the snapshot (the descriptor words, the // flattened SRT, the user data, the uniform fill) and, for the vertex family, the V# table the // attributes are resolved from. -std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapshot& snapshot) { +std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapshot& snapshot, bool masked) { std::uint64_t hash = 0xcbf29ce484222325ull; const auto mix = [&](std::uint64_t value) { hash ^= value; @@ -1411,11 +1673,28 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh for (std::uint32_t i = 0; i < value.dwordCount && i < value.dwords.size(); ++i) mix(value.dwords[i]); } }; - mixDescriptors(snapshot.buffers); + if (masked) { + mix(snapshot.buffers.size()); + for (const auto& value : snapshot.buffers) { + mix(value.dwordCount); + mix(value.availableDwordMask); + const bool present = value.dwords[2] != 0u && (value.dwords[0] != 0u || (value.dwords[1] & 0xffffu) != 0u); + mix(present ? 4u | (value.dwords[0] & 3u) : 0u); + mix(value.dwords[1] & 0xffff0000u); + for (std::uint32_t i = 3; i < value.dwordCount && i < value.dwords.size(); ++i) mix(value.dwords[i]); + } + } else { + mixDescriptors(snapshot.buffers); + } mixDescriptors(snapshot.images); mixDescriptors(snapshot.samplers); - mixWords(snapshot.flattenedSrt); - mixWords(snapshot.userData); + if (masked) { + mix(snapshot.flattenedSrt.size()); + mix(snapshot.userData.size()); + } else { + mixWords(snapshot.flattenedSrt); + mixWords(snapshot.userData); + } mix(static_cast(snapshot.uniformFill.kind)); mix(snapshot.uniformFill.resource); for (const auto stride : snapshot.uniformFill.groupStride) mix(stride); @@ -1474,17 +1753,17 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh // as good). `memoHit` reports the hit. std::shared_ptr materializePreparedMemoized(SourceEntry& source, const std::shared_ptr& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot, bool cacheHit, bool* memoHit) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; - const auto hash = snapshotHash(request, snapshot); - std::uint64_t index = 0; + const auto variantId = variant->artifact.variantId; + const auto hash = snapshotHash(request, snapshot, false); + const auto index = (variantId * 0x9e3779b97f4a7c15ull) ^ hash; auto& counters = resultMemoCounters(); { std::lock_guard lock(source.mutex); - index = (variant->artifact.variantId * 0x9e3779b97f4a7c15ull) ^ hash; - if (auto* set = memoSet(source, index)) { + if (auto* set = memoSet(source.memo, index)) { for (std::size_t way = 0; way < ResultMemoWays; ++way) { auto& entry = set[way]; - if (entry.result == nullptr || entry.variantId != variant->artifact.variantId || entry.hash != hash) continue; - entry.used = ++source.memoClock; + if (entry.result == nullptr || entry.variantId != variantId || entry.hash != hash) continue; + entry.used = nextMemoUse(); counters.hits.fetch_add(1, std::memory_order_relaxed); if (memoHit != nullptr) *memoHit = true; reportResultMemo(); @@ -1492,33 +1771,78 @@ std::shared_ptr materializePreparedMemoized(SourceEntry& } } } - const auto started = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - auto result = std::make_shared(materializeResult(*variant, request, snapshot)); - result->cacheHit = cacheHit; - if (profile) counters.populateNanoseconds.fetch_add(static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()), std::memory_order_relaxed); - counters.misses.fetch_add(1, std::memory_order_relaxed); - std::shared_ptr shared = std::move(result); - { + const bool masking = MaskedResultMemo(); + const auto maskedHash = masking ? snapshotHash(request, snapshot, true) : 0u; + const auto maskedIndex = (variantId * 0x9e3779b97f4a7c15ull) ^ maskedHash; + std::shared_ptr donor; + std::shared_ptr donorPatch; + if (masking) { std::lock_guard lock(source.mutex); - if (source.memo == nullptr) source.memo = std::make_unique(); - auto* set = memoSet(source, index); - auto* victim = set; - bool present = false; - for (std::size_t way = 0; way < ResultMemoWays && !present; ++way) { - auto& entry = set[way]; - if (entry.result != nullptr && entry.variantId == variant->artifact.variantId && entry.hash == hash) { - entry.used = ++source.memoClock; - shared = entry.result; - present = true; - } else if (entry.used < victim->used) { - victim = &entry; + if (auto* set = memoSet(source.maskedMemo, maskedIndex)) { + for (std::size_t way = 0; way < ResultMemoWays; ++way) { + auto& entry = set[way]; + if (entry.result == nullptr || entry.variantId != variantId || entry.hash != maskedHash) continue; + entry.used = nextMemoUse(); + donor = entry.result; + donorPatch = entry.patch; + break; } } - if (!present) { - if (victim->result != nullptr) counters.evictions.fetch_add(1, std::memory_order_relaxed); - *victim = {variant->artifact.variantId, hash, shared, ++source.memoClock}; - } } + const auto started = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + std::shared_ptr result; + std::shared_ptr patch; + if (donor != nullptr) { + result = std::make_shared(*donor); + applyMemoPatch(*result, *donorPatch, snapshot); + if (VerifyMaskedResultMemo()) verifyPatchedResult(*result, materializeResult(*variant, request, snapshot), variantId); + if (profile) counters.patchNanoseconds.fetch_add(static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()), std::memory_order_relaxed); + counters.patched.fetch_add(1, std::memory_order_relaxed); + } else { + if (masking) patch = std::make_shared(); + result = std::make_shared(materializeResult(*variant, request, snapshot, false, patch.get())); + if (profile) counters.populateNanoseconds.fetch_add(static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()), std::memory_order_relaxed); + counters.misses.fetch_add(1, std::memory_order_relaxed); + } + result->cacheHit = cacheHit; + std::shared_ptr shared = std::move(result); + const auto bytes = resultBytes(*shared); + const auto insert = [&](std::unique_ptr& table, std::uint64_t at, std::uint64_t key, std::shared_ptr recipe, bool count) { + if (table == nullptr) { + table = std::make_unique(); + if (source.memo == nullptr || source.maskedMemo == nullptr) { + auto& registry = resultMemoRegistry(); + std::lock_guard registryLock(registry.mutex); + registry.sources.push_back(source.weak_from_this()); + } + } + auto* set = memoSet(table, at); + auto* victim = set; + for (std::size_t way = 0; way < ResultMemoWays; ++way) { + auto& entry = set[way]; + if (entry.result != nullptr && entry.variantId == variantId && entry.hash == key) { + entry.used = nextMemoUse(); + return entry.result; + } + if (entry.used < victim->used) victim = &entry; + } + if (victim->result != nullptr) { + if (count) counters.evictions.fetch_add(1, std::memory_order_relaxed); + counters.heldBytes.fetch_sub(victim->bytes, std::memory_order_relaxed); + counters.heldEntries.fetch_sub(1, std::memory_order_relaxed); + } + const auto held = bytes + (recipe != nullptr ? patchBytes(*recipe) : 0u); + *victim = {variantId, key, shared, nextMemoUse(), std::move(recipe), held}; + counters.heldBytes.fetch_add(held, std::memory_order_relaxed); + counters.heldEntries.fetch_add(1, std::memory_order_relaxed); + return shared; + }; + { + std::lock_guard lock(source.mutex); + shared = insert(source.memo, index, hash, nullptr, true); + if (patch != nullptr && patch->valid) static_cast(insert(source.maskedMemo, maskedIndex, maskedHash, patch, false)); + } + if (const auto budget = resultMemoBudget(); budget != 0 && counters.heldBytes.load(std::memory_order_relaxed) > budget) trimResultMemo(budget); reportResultMemo(); return shared; } @@ -1751,4 +2075,18 @@ std::shared_ptr Recompile(const RecompileRequest& request return recompileReporting(request, [&] { return RecompileImpl(request, capture, memoHit); }); } +void SetResultMemoBudget(std::uint64_t bytes) { + resultMemoBudgetBytes().store(bytes, std::memory_order_relaxed); + if (bytes != 0 && resultMemoCounters().heldBytes.load(std::memory_order_relaxed) > bytes) trimResultMemo(bytes); +} + +void SetResultMemoTrimHook(std::function hook) { + std::lock_guard lock(resultMemoTrimMutex()); + resultMemoTrimHook() = std::move(hook); +} + +std::uint64_t ResultMemoBytes() { + return resultMemoCounters().heldBytes.load(std::memory_order_relaxed); +} + } diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 9462ba0721..6d3cc50544 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -127,7 +128,8 @@ enum class ColorExportPacking : std::uint8_t { None, Unorm10_11_11, Float11_11_10, - Unorm4_4_4_4 + Unorm4_4_4_4, + Uint }; enum class RasterPrimitive : std::uint8_t { @@ -603,6 +605,8 @@ inline std::uint64_t HashSpirvWords(std::span words, std::u // reference, or the per-dispatch copy this class removes comes back without a compiler hint. // ContentHash is computed once per shared holder, on first use, and dropped by a write. class SharedSpirv { + struct Holder; + public: SharedSpirv() = default; SharedSpirv(std::vector words) : holder(std::make_shared(std::move(words), nullptr, std::span{})) {} @@ -639,6 +643,25 @@ public: std::vector::iterator insert(std::vector::const_iterator where, std::initializer_list values) { return Mutable().insert(where, values); } friend bool operator==(const SharedSpirv& left, const SharedSpirv& right) { return std::ranges::equal(left.Words(), right.Words()); } + class Weak { + public: + [[nodiscard]] SharedSpirv Lock() const { + SharedSpirv spirv; + spirv.holder = holder.lock(); + return spirv; + } + [[nodiscard]] bool Expired() const { return holder.expired(); } + + private: + friend class SharedSpirv; + std::weak_ptr holder; + }; + [[nodiscard]] Weak Weaken() const { + Weak weak; + weak.holder = holder; + return weak; + } + private: struct Holder { Holder(std::vector words, std::shared_ptr owner, std::span view) : words(std::move(words)), owner(std::move(owner)), view(view) {} @@ -805,6 +828,9 @@ struct MeshOutputBudget { struct ResourceCapture; [[nodiscard]] std::shared_ptr Recompile(const RecompileRequest& request, const ResourceCapture& capture, bool* memoHit = nullptr); +void SetResultMemoBudget(std::uint64_t bytes); +[[nodiscard]] std::uint64_t ResultMemoBytes(); +void SetResultMemoTrimHook(std::function hook); // Debug aid (see DebugProbe in Translation/TranslationContext.hpp): the APS5_PROBE register probe is // only applied while a driver has it active, so it can be limited to one dispatch; the recompile diff --git a/core/shader/recompiler/ShaderCacheVersion.cmake b/core/shader/recompiler/ShaderCacheVersion.cmake index f4477ed1e1..b3cbea2c75 100644 --- a/core/shader/recompiler/ShaderCacheVersion.cmake +++ b/core/shader/recompiler/ShaderCacheVersion.cmake @@ -36,11 +36,10 @@ endforeach() set(failures) set(manifest "${EXTRA}\n") set(switches) -foreach(source IN LISTS sources) +foreach(source sourceReal IN ZIP_LISTS sources sourcesReal) if(NOT EXISTS "${source}") continue() endif() - get_filename_component(sourceReal "${source}" REALPATH) file(READ "${source}" text) string(REPLACE "\r\n" "\n" text "${text}") string(SHA256 digest "${text}") @@ -65,7 +64,11 @@ foreach(source IN LISTS sources) set(resolved) foreach(directory IN ITEMS "${sourceDirectory}" ${includeDirectories}) if(EXISTS "${directory}/${included}" AND NOT IS_DIRECTORY "${directory}/${included}") - get_filename_component(resolved "${directory}/${included}" REALPATH) + string(SHA1 includedKey "${directory}/${included}") + if(NOT DEFINED includedReal_${includedKey}) + get_filename_component(includedReal_${includedKey} "${directory}/${included}" REALPATH) + endif() + set(resolved "${includedReal_${includedKey}}") break() endif() endforeach() diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index aca1cb4eea..ce6863e337 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -1380,6 +1380,7 @@ constexpr std::string_view NeutralSwitches[] = { "APS5_NO_RESULT_MEMO", "APS5_NO_TABLE_RESOLUTION_REUSE", "APS5_NO_TABLE_SNAPSHOT_REUSE", + "APS5_RESULT_MEMO_MB", }; const std::vector& switchKey() { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 601bed36a2..cd5552084d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -1133,6 +1133,9 @@ void EmitWriteOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { auto& state = ctx.state; const bool uintImage = access.image.numericClass == IrTextureNumericClass::Uint; auto condition = ctx.Arg(access.inst, 3); + if (access.mem.imageDimension == RdnaImageDimension::Dim2DArray && access.image.dimension == RdnaImageDimension::Dim1D) { + condition = Binary(state, spv::OpLogicalAnd, TypeBool(state), condition, Binary(state, spv::OpIEqual, TypeBool(state), AddressU32(ctx, access, 2u), ConstantU32(state, 0u))); + } if (VolumeSliceAddress(access) || ArraySliceAddress(access)) { if (access.image.mipMode == ImageMipMode::DynamicStorage) ctx.Fail(access.inst, "stores a 2D-array address to a 3D image or a 3D address to a 2D-array image with dynamic mips, which is not implemented"); const auto image = state.module.AllocateId(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 1bc242f265..e12ee79af6 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -444,11 +444,12 @@ std::uint32_t PackExport(SpirvEmitterState& state, IrBufferFormat format, std::u std::uint32_t SelectExportPacking(SpirvEmitterState& state, std::uint32_t target, std::uint32_t color) { const auto packing = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ExportPackingBase + target, static_cast(ColorExportPacking::None)); - const auto packed = EmitCompareU32Constant(state, spv::OpINotEqual, packing, 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)); const auto small = EmitCompareU32Constant(state, spv::OpIEqual, packing, static_cast(ColorExportPacking::Unorm4_4_4_4)); 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); std::array components{}; for (std::uint32_t component = 0; component < components.size(); ++component) { const auto unpacked = state.module.AllocateId(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index f3736dc698..01fe275817 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -107,7 +107,9 @@ RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& inst } if (memory.gpuDescriptor) faultIf(context, instruction, invalid, base, 0u, BdaAbi::FaultReason::InvalidDescriptor); const auto stride = field(state, word1, 16u, 14u); - const auto present = runtime ? both(state, nonzero(state, context.Arg(*handle, 2u)), Binary(state, spv::OpLogicalOr, TypeBool(state), nonzero(state, context.Arg(*handle, 0u)), nonzero(state, field(state, word1, 0u, 16u)))) : nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u)); + auto baseHigh = runtime ? word1 : context.Arg(*handle, 1u); + if (!runtime && GcnGuestShaders()) baseHigh = Binary(state, spv::OpBitwiseAnd, u32, baseHigh, ConstantU32(state, GcnBufferWord1Mask)); + const auto present = both(state, nonzero(state, context.Arg(*handle, 2u)), Binary(state, spv::OpLogicalOr, TypeBool(state), nonzero(state, context.Arg(*handle, 0u)), nonzero(state, field(state, baseHigh, 0u, 16u)))); const auto valid = memory.gpuDescriptor ? both(state, Unary(state, spv::OpLogicalNot, TypeBool(state), invalid), Binary(state, spv::OpINotEqual, TypeBool(state), base, BdaConstant(state, 0u))) : present; return {base, context.Arg(*handle, 2u), stride, word3, both(state, nonzero(state, stride), nonzero(state, field(state, word1, 31u, 1u))), index, valid}; } @@ -668,8 +670,7 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr const auto word1 = state.module.SpecializationConstant(u32, first, 0u); const auto stride = field(state, word1, 16u, 14u); const auto swizzle = both(state, nonzero(state, stride), nonzero(state, field(state, word1, 31u, 1u))); - const auto valid = nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u)); - shared = {ConstantU32(state, 0u), parameters[3], stride, state.module.SpecializationConstant(u32, first + 1u, 0u), swizzle, parameters[0], valid, parameters[1], parameters[2]}; + shared = {ConstantU32(state, 0u), parameters[3], stride, state.module.SpecializationConstant(u32, first + 1u, 0u), swizzle, parameters[0], nonzero(state, parameters[8]), parameters[1], parameters[2]}; } const auto value = formatSwitch(context, instruction, shared, components, store, store ? parameters[9] : 0u); if (store) state.module.AddFunction(spv::OpReturn); diff --git a/core/shader/recompiler/StencilReplay.cpp b/core/shader/recompiler/StencilReplay.cpp index 30bc43b765..39acca62dc 100644 --- a/core/shader/recompiler/StencilReplay.cpp +++ b/core/shader/recompiler/StencilReplay.cpp @@ -108,7 +108,7 @@ public: } for (const auto& info : program.Metadata().blockInfo) { if (info.terminator.kind != TerminatorKind::ConditionalBranch) continue; - if (!control(info)) return false; + if (!controlProven(info)) return false; } for (const auto& [value, mask] : ownMasks) if (helperObserved(value)) return false; for (const auto* block : program.BlockOrder()) { @@ -132,6 +132,60 @@ private: if (value && !value->HasImmediate()) restricted.insert(value); } + void restrictSource(const IrValue* value, unsigned depth) { + value = value->Resolve(); + if (depth > 64u || value->HasImmediate()) return; + restrict(value); + for (const auto* argument : value->Arguments()) restrictSource(argument, depth + 1u); + } + + bool execEquivalent(const IrValue* left, const IrValue* right) const { + const IrBlock* block = left->Parent(); + if (block == nullptr || block != right->Parent()) return false; + bool between = false; + for (const IrValue* instruction : block->Instructions()) { + if (instruction == left || instruction == right) { + if (between) return true; + between = true; + continue; + } + if (!between) continue; + switch (instruction->Opcode()) { + case IrOpcode::SetExec: + case IrOpcode::SetExecLo: + case IrOpcode::SetExecHi: + case IrOpcode::Discard: + case IrOpcode::Sendmsg: + case IrOpcode::SetThreadBitScalarRegister: + case IrOpcode::SetScalarMaskTag: + return false; + default: + break; + } + } + return false; + } + + bool equivalentSource(const IrValue* left, const IrValue* right, unsigned depth) { + if (depth > 64u) return false; + left = left->Resolve(); + right = right->Resolve(); + if (left == right || sameValue(left, right)) return true; + if (left->HasImmediate() || right->HasImmediate()) return left->HasImmediate() && right->HasImmediate() && left->Type() == right->Type() && left->ImmediateU64() == right->ImmediateU64(); + if (left->Opcode() != right->Opcode() || left->ArgumentCount() != right->ArgumentCount()) return false; + if (left->Opcode() == IrOpcode::Ballot && !execEquivalent(left, right)) return false; + for (std::size_t index = 0; index < left->ArgumentCount(); ++index) if (!equivalentSource(left->Argument(index), right->Argument(index), depth + 1u)) return false; + return true; + } + + bool equivalentWqm(const IrValue* left, const IrValue* right, unsigned depth) { + left = left->Resolve(); + right = right->Resolve(); + if (left == right) return true; + if (left->Opcode() != IrOpcode::WqmU64 || right->Opcode() != IrOpcode::WqmU64 || left->ArgumentCount() != 1u || right->ArgumentCount() != 1u) return false; + return equivalentSource(left->Argument(0), right->Argument(0), depth + 1u); + } + std::optional own(const IrValue* value) { value = value->Resolve(); if (value->Opcode() != IrOpcode::INotEqual32) return std::nullopt; @@ -189,7 +243,8 @@ private: valid = (maskStable(pair(0u, 0u), depth + 1u) && maskStable(pair(1u, 1u), depth + 1u)) || (waveSize != 32u && maskStable(pair(0u, 1u), depth + 1u) && maskStable(pair(1u, 0u), depth + 1u)); } else if (mask.low->Opcode() == IrOpcode::CompositeExtractU64 && mask.low->ArgumentCount() == 2u && Immediate(mask.low->Argument(1), 0u)) { const IrValue* wqm = mask.low->Argument(0)->Resolve(); - const bool paired = waveSize == 32u || (mask.high->Opcode() == IrOpcode::CompositeExtractU64 && mask.high->ArgumentCount() == 2u && mask.high->Argument(0)->Resolve() == wqm && Immediate(mask.high->Argument(1), 1u)); + const IrValue* highWqm = mask.high->Opcode() == IrOpcode::CompositeExtractU64 && mask.high->ArgumentCount() == 2u ? mask.high->Argument(0)->Resolve() : nullptr; + const bool paired = waveSize == 32u || (highWqm != nullptr && Immediate(mask.high->Argument(1), 1u) && equivalentWqm(wqm, highWqm, depth)); if (paired && wqm->Opcode() == IrOpcode::WqmU64 && wqm->ArgumentCount() == 1u) { const IrValue* source = wqm->Argument(0)->Resolve(); valid = source->Opcode() == IrOpcode::CompositeConstructU64 && source->ArgumentCount() == 2u && maskStable(ReplayMask{source->Argument(0)->Resolve(), source->Argument(1)->Resolve()}, depth + 1u); @@ -197,7 +252,11 @@ private: const auto provenance = helpers(ReplayMask{source->Argument(0)->Resolve(), source->Argument(1)->Resolve()}, depth + 1u); valid = provenance == HelperMask::RealOnly || provenance == HelperMask::QuadStable; } - if (valid) { restrict(wqm); restrict(source); } + if (valid) { + restrict(wqm); + restrict(source); + if (highWqm != wqm) restrictSource(highWqm, 0u); + } } } else if (mask.low->IsPhi() && mask.low->ArgumentCount() != 0u && mask.low->ArgumentCount() == mask.low->PhiBlockCount() && (waveSize == 32u || (mask.high->IsPhi() && mask.high->Parent() == mask.low->Parent() && mask.high->ArgumentCount() == mask.high->PhiBlockCount()))) { valid = true; @@ -288,7 +347,8 @@ private: } } else if (mask.low->Opcode() == IrOpcode::CompositeExtractU64 && mask.low->ArgumentCount() == 2u && Immediate(mask.low->Argument(1), 0u)) { const IrValue* wqm = mask.low->Argument(0)->Resolve(); - const bool paired = waveSize == 32u || (mask.high->Opcode() == IrOpcode::CompositeExtractU64 && mask.high->ArgumentCount() == 2u && mask.high->Argument(0)->Resolve() == wqm && Immediate(mask.high->Argument(1), 1u)); + const IrValue* highWqm = mask.high->Opcode() == IrOpcode::CompositeExtractU64 && mask.high->ArgumentCount() == 2u ? mask.high->Argument(0)->Resolve() : nullptr; + const bool paired = waveSize == 32u || (highWqm != nullptr && Immediate(mask.high->Argument(1), 1u) && equivalentWqm(wqm, highWqm, depth)); if (paired && wqm->Opcode() == IrOpcode::WqmU64 && wqm->ArgumentCount() == 1u) { const IrValue* source = wqm->Argument(0)->Resolve(); if (source->Opcode() == IrOpcode::CompositeConstructU64 && source->ArgumentCount() == 2u) { @@ -433,24 +493,89 @@ private: return left == right || (left->HasImmediate() && right->HasImmediate() && left->Type() == IrType::U32 && right->Type() == IrType::U32 && left->ImmediateU32() == right->ImmediateU32()); } - bool inactiveEquals(const IrValue* value, const IrValue* other, const IrValue* predicate, unsigned depth = 0) { + bool inactiveEquals(const IrValue* value, const IrValue* other, const IrValue* predicate, const std::set& region, unsigned depth = 0) { if (depth > 32u) return false; value = value->Resolve(); if (sameValue(value, other)) return true; - if ((value->Opcode() == IrOpcode::SelectU32 || value->Opcode() == IrOpcode::SelectF32 || value->Opcode() == IrOpcode::SelectU1) && value->ArgumentCount() == 3u && (value->Argument(0)->Resolve() == predicate->Resolve() || GuardImplies(program, value->Argument(0), predicate))) return inactiveEquals(value->Argument(2), other, predicate, depth + 1u); + if (value->Type() == IrType::Bool && other->Resolve() == predicate->Resolve() && GuardImplies(program, value, predicate)) return true; + if ((value->Opcode() == IrOpcode::SelectU32 || value->Opcode() == IrOpcode::SelectF32 || value->Opcode() == IrOpcode::SelectU1) && value->ArgumentCount() == 3u && (value->Argument(0)->Resolve() == predicate->Resolve() || GuardImplies(program, value->Argument(0), predicate))) return inactiveEquals(value->Argument(2), other, predicate, region, depth + 1u); + if (value->IsPhi() && region.contains(value->Parent()) && value->ArgumentCount() != 0u && value->ArgumentCount() == value->PhiBlockCount()) { + return std::all_of(value->Arguments().begin(), value->Arguments().end(), [&](const IrValue* argument) { return inactiveEquals(argument, other, predicate, region, depth + 1u); }); + } return false; } - bool quadObserved(const IrValue* value) const { + bool explicitLodSample(const IrValue* value) const { + if (value->Opcode() == IrOpcode::ImageSampleExplicitLod) return true; + if (value->Opcode() != IrOpcode::ImageSampleRaw && value->Opcode() != IrOpcode::ImageGatherRaw) return false; + const auto index = value->Flags().index; + return index < program.Resources().memoryInfo.size() && ImageSampleExplicitLod(program.Resources().memoryInfo[index].imageSampleFlags, program.Resources().stage); + } + + bool ownBitShifted(const IrValue* user, std::size_t operand, std::vector& terminals) const { + if (user->Opcode() != IrOpcode::ShiftRightLogical32 || user->ArgumentCount() != 2u || operand != 0u) return false; + const IrValue* amount = user->Argument(1)->Resolve(); + if (amount->Opcode() != IrOpcode::BitwiseAnd32) return false; + const IrValue* lane = Other(amount, 31u); + if (lane == nullptr || lane->Opcode() != IrOpcode::LaneId) return false; + for (const auto& use : user->OperandUses()) { + const IrValue* isolated = use.user; + if (isolated->Opcode() != IrOpcode::BitwiseAnd32 || Other(isolated, 1u) != user) return false; + terminals.push_back(isolated); + } + return true; + } + + bool ownBitSelected(const IrValue* user, std::size_t operand, unsigned half, std::vector& terminals) const { + if (user->Opcode() != IrOpcode::SelectU32 || user->ArgumentCount() != 3u || operand != half + 1u) return false; + const IrValue* condition = user->Argument(0)->Resolve(); + if (condition->Opcode() != IrOpcode::ULessThan32 || condition->ArgumentCount() != 2u || condition->Argument(0)->Resolve()->Opcode() != IrOpcode::LaneId || !Immediate(condition->Argument(1), 32u)) return false; + for (const auto& use : user->OperandUses()) if (!ownBitShifted(use.user, use.operand, terminals)) return false; + return true; + } + + bool ownBitBallot(const IrValue* ballot, std::vector& terminals) const { + if (ballot->Opcode() != IrOpcode::Ballot || ballot->ArgumentCount() != 1u) return false; + for (const auto& use : ballot->OperandUses()) { + const IrValue* extract = use.user; + if (extract->Opcode() != IrOpcode::CompositeExtractU32x4 || extract->ArgumentCount() != 2u || extract->Argument(0)->Resolve() != ballot) return false; + const IrValue* index = extract->Argument(1)->Resolve(); + if (!index->HasImmediate() || index->ImmediateU32() >= (waveSize == 32u ? 1u : 2u)) return false; + const auto half = index->ImmediateU32(); + for (const auto& euse : extract->OperandUses()) { + const IrValue* word = euse.user; + if (word->Opcode() == IrOpcode::BitwiseAnd32) { + for (const auto& muse : word->OperandUses()) { + const bool read = waveSize == 64u ? ownBitSelected(muse.user, muse.operand, half, terminals) : ownBitShifted(muse.user, muse.operand, terminals); + if (!read) return false; + } + } else { + const bool read = waveSize == 64u ? ownBitSelected(word, euse.operand, half, terminals) : ownBitShifted(word, euse.operand, terminals); + if (!read) return false; + } + } + } + return true; + } + + bool quadObserved(const IrValue* value) { std::set visited; std::vector pending{value}; while (!pending.empty()) { const IrValue* current = pending.back(); pending.pop_back(); if (!visited.insert(current).second) continue; - for (const auto& info : program.Metadata().blockInfo) if (info.condition && info.condition->Resolve() == current) return true; + for (const auto& info : program.Metadata().blockInfo) if (info.condition && info.condition->Resolve() == current && !controlProven(info)) return true; for (const auto& use : current->OperandUses()) { const auto opcode = use.user->Opcode(); + if (opcode == IrOpcode::Ballot) { + std::vector terminals; + if (ownBitBallot(use.user, terminals)) { + pending.insert(pending.end(), terminals.begin(), terminals.end()); + continue; + } + } + if (ImageOpcodeInfoOf(opcode).needsSampler && explicitLodSample(use.user)) { pending.push_back(use.user); continue; } if (ImageOpcodeInfoOf(opcode).needsSampler || opcode == IrOpcode::Ballot || opcode == IrOpcode::WqmU64 || opcode == IrOpcode::ReadLane || opcode == IrOpcode::ReadFirstLane || opcode == IrOpcode::DppMoveU32 || opcode == IrOpcode::DppUpdateU32 || opcode == IrOpcode::SwizzleU32 || opcode == IrOpcode::PermuteU32 || opcode == IrOpcode::BpermuteU32 || opcode == IrOpcode::Permlane16U32 || opcode == IrOpcode::WriteLane || SharedAccessOf(opcode) != SharedAccess::None) return true; if (opcode == IrOpcode::SetAttribute || opcode == IrOpcode::Reference || opcode == IrOpcode::ReferenceU32) continue; pending.push_back(use.user); @@ -473,6 +598,7 @@ private: } for (const auto& use : current->OperandUses()) { const auto opcode = use.user->Opcode(); + if (ImageOpcodeInfoOf(opcode).needsSampler && explicitLodSample(use.user)) { pending.push_back(use.user); continue; } if (ImageOpcodeInfoOf(opcode).needsSampler) return true; if (restricted.contains(use.user) || opcode == IrOpcode::SetAttribute || opcode == IrOpcode::Reference || opcode == IrOpcode::ReferenceU32) continue; pending.push_back(use.user); @@ -482,31 +608,58 @@ private: } bool optionalBody(const IrBlock* branch, const IrBlock* body, const IrBlock* merge, const IrValue* predicate) { - const auto& bodyInfo = program.Metadata().blockInfo[blockIndices.at(body)]; - if (bodyInfo.terminator.kind != TerminatorKind::Branch || indices.at(bodyInfo.terminator.trueBlock) != blockIndices.at(merge)) return false; + if (body == merge) return false; + std::set region; + std::vector pending{body}; + while (!pending.empty()) { + const IrBlock* block = pending.back(); + pending.pop_back(); + if (block == merge || !region.insert(block).second) continue; + if (block == branch || region.size() > 64u || program.Metadata().blockInfo[blockIndices.at(block)].terminator.kind == TerminatorKind::Return) return false; + for (const auto* successor : block->Successors()) pending.push_back(successor); + } + for (const auto* block : region) { + for (const auto* predecessor : block->Predecessors()) { + if (block == body ? predecessor != branch : !region.contains(predecessor)) return false; + } + } std::set merged; for (const auto* instruction : merge->Instructions()) { if (!instruction->IsPhi()) continue; - if (instruction->ArgumentCount() != 2u || instruction->PhiBlockCount() != 2u) return false; + if (instruction->ArgumentCount() < 2u || instruction->ArgumentCount() != instruction->PhiBlockCount()) return false; const IrValue* skipped = nullptr; - const IrValue* taken = nullptr; - for (std::size_t index = 0; index < 2u; ++index) { + std::vector taken; + for (std::size_t index = 0; index < instruction->PhiBlockCount(); ++index) { if (instruction->PhiBlock(index) == branch) skipped = instruction->Argument(index)->Resolve(); - if (instruction->PhiBlock(index) == body) taken = instruction->Argument(index)->Resolve(); + else if (region.contains(instruction->PhiBlock(index))) taken.push_back(instruction->Argument(index)->Resolve()); + else return false; } - if (!skipped || !taken || !inactiveEquals(taken, skipped, predicate) || quadObserved(instruction)) return false; + if (!skipped || taken.empty()) return false; + if (quadObserved(instruction)) return false; + for (const auto* value : taken) if (!inactiveEquals(value, skipped, predicate, region)) return false; merged.insert(instruction); } - for (const auto* instruction : body->Instructions()) { - if (instruction->Opcode() == IrOpcode::SetAttribute || instruction->Opcode() == IrOpcode::Discard) return false; - for (const auto& use : instruction->OperandUses()) { - if (use.user->Parent() == body || merged.contains(use.user) || use.user->Opcode() == IrOpcode::Reference || use.user->Opcode() == IrOpcode::ReferenceU32) continue; - return false; + for (const auto* block : region) { + for (const auto* instruction : block->Instructions()) { + if (instruction->Opcode() == IrOpcode::SetAttribute || instruction->Opcode() == IrOpcode::Discard) return false; + for (const auto& use : instruction->OperandUses()) { + if (region.contains(use.user->Parent()) || merged.contains(use.user) || use.user->Opcode() == IrOpcode::Reference || use.user->Opcode() == IrOpcode::ReferenceU32) continue; + return false; + } } } return !merged.empty(); } + bool controlProven(const BlockInfo& info) { + if (const auto found = controlResults.find(info.id); found != controlResults.end()) return found->second; + if (!controlVisiting.insert(info.id).second) return false; + const bool result = control(info); + controlVisiting.erase(info.id); + controlResults[info.id] = result; + return result; + } + bool control(const BlockInfo& info) { const auto* branch = program.BlockOrder()[indices.at(info.id)]; const auto* trueBlock = program.BlockOrder()[indices.at(info.terminator.trueBlock)]; @@ -553,6 +706,8 @@ private: std::map indices; std::map blockIndices; std::vector> allPaths; + std::map controlResults; + std::set controlVisiting; }; std::optional> Values(const IrProgram& program, const IrValue* value, std::set& visiting, unsigned depth, std::optional component = {}, const IrValue* active = nullptr) { diff --git a/core/shader/recompiler/Translation/src/RoundingMode.cpp b/core/shader/recompiler/Translation/src/RoundingMode.cpp index 0edeca52f0..0e648acaa9 100644 --- a/core/shader/recompiler/Translation/src/RoundingMode.cpp +++ b/core/shader/recompiler/Translation/src/RoundingMode.cpp @@ -89,6 +89,7 @@ RoundingUse RoundingUseOf(RdnaOpcode opcode) { case RdnaOpcode::VCvtF32I32: case RdnaOpcode::VCvtF32U32: case RdnaOpcode::VLdexpF32: + case RdnaOpcode::VCvtPkU8F32: return {round32, RoundingUseKind::Directed32}; case RdnaOpcode::VCvtF16F32: case RdnaOpcode::VCvtF16U16: @@ -120,7 +121,6 @@ RoundingUse RoundingUseOf(RdnaOpcode opcode) { case RdnaOpcode::VMadmkF32: case RdnaOpcode::VMadakF32: case RdnaOpcode::VFractF32: - case RdnaOpcode::VCvtPkU8F32: case RdnaOpcode::VCvtF32F64: case RdnaOpcode::VDivScaleF32: case RdnaOpcode::VDivFmasF32: diff --git a/core/shader/recompiler/Translation/src/VectorInstructions.cpp b/core/shader/recompiler/Translation/src/VectorInstructions.cpp index f94148689f..804535419f 100644 --- a/core/shader/recompiler/Translation/src/VectorInstructions.cpp +++ b/core/shader/recompiler/Translation/src/VectorInstructions.cpp @@ -111,7 +111,6 @@ std::uint32_t TranslationContext::f32DenormalFlushFor(const RdnaInstruction& ins case RdnaOpcode::VCmpxNleF32: case RdnaOpcode::VCmpxNlgF32: case RdnaOpcode::VCmpxNltF32: - return floatMode.has_value() && ((floatMode->floatMode >> 4u) & 1u) != 0u ? 0u : 1u; case RdnaOpcode::VCvtI32F32: case RdnaOpcode::VCvtU32F32: case RdnaOpcode::VCvtRpiI32F32: @@ -123,6 +122,7 @@ std::uint32_t TranslationContext::f32DenormalFlushFor(const RdnaInstruction& ins case RdnaOpcode::VCvtPkU8F32: case RdnaOpcode::VCvtPkrtzF16F32: case RdnaOpcode::VCvtF16F32: + return floatMode.has_value() && ((floatMode->floatMode >> 4u) & 1u) != 0u ? 0u : 1u; case RdnaOpcode::VDivScaleF32: case RdnaOpcode::VDivFixupF32: case RdnaOpcode::VMadMixloF16: diff --git a/demo/web/app.mjs b/demo/web/app.mjs index 4e0b534145..56e33dabf0 100644 --- a/demo/web/app.mjs +++ b/demo/web/app.mjs @@ -67,8 +67,9 @@ function update() { const totalBytes = sum(summaries.map(summary => summary.totalBytes)); const modules = sum(summaries.map(summary => summary.modules)); const sizeText = value => value === null ? "beyond the supported size range" : bytes(value); + const countText = value => value === null ? "beyond the supported range" : value; $("archive-info").textContent = multi - ? `${selectedInputs.length} apps · ${sizeText(totalFiles)} files · ${sizeText(totalBytes)} expanded · ${sizeText(modules)} modules · ${sizeText(selectedBytes)} ${target} conversion input` + ? `${selectedInputs.length} apps · ${countText(totalFiles)} files · ${sizeText(totalBytes)} expanded · ${countText(modules)} modules · ${sizeText(selectedBytes)} ${target} conversion input` : `${summaries[0].files} files · ${bytes(summaries[0].totalBytes)} expanded · ${summaries[0].modules} modules · ${bytes(target === "windows" ? summaries[0].windowsSelectedBytes : summaries[0].selectedBytes)} ${target} conversion input` + (summaries[0].root ? ` · Root: ${summaries[0].root}` : ""); } $("save-note").textContent = multi ? $("save").value === "memory" ? "Creates one ZIP with each app in its own folder. The complete bundle must fit in the 256 MiB memory-download budget." : "Writes one compressed ZIP containing each app in its own folder, without holding the whole bundle in memory." : $("save").value === "disk" ? "Creates a subfolder with the executable, libs and app0. No ZIP compression. Incomplete output folders are removed after cancellation or failure. Browsers may block DLL and other restricted files; use ZIP for those apps." : $("save").value === "zip" ? "Writes a compressed ZIP directly to disk without holding the whole archive in memory." : "Up to 256 MiB including expanded files and output overhead."; @@ -262,12 +263,16 @@ async function selectInputs(inputs) { $("choose").onclick = () => { $("file").value = ""; $("file").click(); }; $("change").onclick = () => { if (working) return; + worker?.terminate(); + worker = null; if (downloadUrl) { URL.revokeObjectURL(downloadUrl); downloadUrl = null; } - file = null; - summary = null; + selectedInputs = []; + summaries = []; + inspectionComplete = false; $("file").value = ""; $("folder").value = ""; - for (const id of ["selection", "warnings", "error", "done", "download"]) $(id).hidden = true; + $("app-list").replaceChildren(); + for (const id of ["selection", "warnings", "error", "done", "download", "app-list"]) $(id).hidden = true; $("acknowledge").checked = false; $("drop").hidden = false; update(); diff --git a/demo/web/tests/app.test.mjs b/demo/web/tests/app.test.mjs new file mode 100644 index 0000000000..a97a239ad6 --- /dev/null +++ b/demo/web/tests/app.test.mjs @@ -0,0 +1,140 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { setImmediate } from "node:timers/promises"; +import { test } from "node:test"; + +class Element { + children = []; + listeners = new Map(); + value = ""; + checked = false; + disabled = false; + textContent = ""; + + constructor(hidden = false) { this.hidden = hidden; } + addEventListener(type, listener) { this.listeners.set(type, listener); } + removeAttribute(name) { delete this[name]; } + append(...children) { this.children.push(...children); } + replaceChildren(...children) { this.children = children; } +} + +async function setup(t, name) { + const html = readFileSync(new URL("../index.html", import.meta.url), "utf8"); + const elements = new Map([...html.matchAll(/<[^>]+\bid="([^"]+)"[^>]*>/gu)].map(match => [ + match[1], new Element(/\shidden(?:\s|>)/u.test(match[0])) + ])); + const get = id => { + assert.ok(elements.has(id), `Missing element: ${id}`); + return elements.get(id); + }; + const options = new Map(["disk", "zip", "memory"].map(value => [`[value="${value}"]`, new Element()])); + get("save").querySelector = selector => options.get(selector); + get("folder").webkitdirectory = true; + const workers = []; + class Worker { + messages = []; + terminated = false; + constructor() { workers.push(this); } + postMessage(message) { this.messages.push(message); } + terminate() { this.terminated = true; } + } + const globals = { + document: { + getElementById: get, + createElement: () => new Element(), + querySelector: selector => { + assert.equal(selector, 'input[name="target"]:checked'); + return { value: "linux" }; + } + }, + window: { isSecureContext: true, Worker, WebAssembly, ReadableStream, WritableStream, crypto: { subtle: {} } }, + Worker, + fetch: async () => ({ + ok: true, + json: async () => ({ tag: "test-release", platforms: { linux: { expandedBytes: 1024 }, windows: { expandedBytes: 1024 } } }) + }) + }; + for (const [key, value] of Object.entries(globals)) { + const original = Object.getOwnPropertyDescriptor(globalThis, key); + Object.defineProperty(globalThis, key, { value, configurable: true, writable: true }); + t.after(() => { + if (original) Object.defineProperty(globalThis, key, original); + else delete globalThis[key]; + }); + } + await import(new URL(`../app.mjs?${name}`, import.meta.url)); + await setImmediate(); + return { get, workers }; +} + +async function inspect(worker, count) { + for (let index = 0; index < count; index++) { + await worker.onmessage({ data: { + type: "inspected", index, + summary: { files: 1, totalBytes: 1024, modules: 0, selectedBytes: 1024, windowsSelectedBytes: 1024, warnings: ["Test warning"] } + } }); + } + await worker.onmessage({ data: { type: "inspection-complete" } }); +} + +for (const kind of ["zip", "batch", "folder"]) { + test(`Change resets an inspected ${kind} and permits a new conversion`, async t => { + const { get, workers } = await setup(t, kind); + assert.equal(get("convert").disabled, true); + if (kind === "folder") { + get("folder").files = [{ name: "eboot.bin", webkitRelativePath: "app/eboot.bin" }]; + get("folder").onchange(); + } else { + get("file").files = kind === "batch" ? [{ name: "first.zip" }, { name: "second.zip" }] : [{ name: "first.zip" }]; + get("file").onchange(); + } + const first = workers[0]; + assert.equal(first.messages[0].type, "inspect"); + assert.equal(first.messages[0].inputs.length, kind === "batch" ? 2 : 1); + get("change").onclick(); + assert.equal(first.terminated, false); + await inspect(first, first.messages[0].inputs.length); + assert.equal(get("selection").hidden, false); + assert.equal(get("drop").hidden, true); + assert.equal(get("warnings").hidden, false); + assert.equal(get("app-list").children.length, first.messages[0].inputs.length); + assert.equal(get("app-list").hidden, kind !== "batch"); + get("acknowledge").checked = true; + get("acknowledge").listeners.get("change")(); + assert.equal(get("convert").disabled, false); + + get("change").onclick(); + assert.equal(first.terminated, true); + assert.equal(get("file").value, ""); + assert.equal(get("folder").value, ""); + assert.equal(get("selection").hidden, true); + assert.equal(get("warnings").hidden, true); + assert.equal(get("app-list").hidden, true); + assert.equal(get("app-list").children.length, 0); + assert.equal(get("drop").hidden, false); + assert.equal(get("acknowledge").checked, false); + assert.equal(get("convert").disabled, true); + get("acknowledge").checked = true; + get("acknowledge").listeners.get("change")(); + assert.equal(get("convert").disabled, true); + get("change").onclick(); + + get("file").files = [{ name: "replacement.zip" }]; + get("file").onchange(); + assert.equal(workers.length, 2); + const replacement = workers[1]; + assert.deepEqual(replacement.messages[0].inputs.map(input => input.name), ["replacement.zip"]); + assert.equal(get("convert").disabled, true); + await inspect(replacement, 1); + assert.equal(get("archive-name").textContent, "replacement.zip"); + assert.equal(get("app-list").children.length, 1); + assert.equal(get("app-list").hidden, true); + assert.equal(get("convert").disabled, true); + get("acknowledge").checked = true; + get("acknowledge").listeners.get("change")(); + assert.equal(get("convert").disabled, false); + await get("convert").onclick(); + assert.equal(replacement.messages.at(-1).type, "convert"); + assert.equal(replacement.terminated, false); + }); +} diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 6c47cfa197..e63cb8cf20 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -124,17 +124,17 @@ flowchart LR - [`Recompiler.cpp`](../../core/shader/recompiler/Recompiler.cpp) runs the stages in this order. With `ANYPS5_ENABLE_SPIRV_TOOLS`, the SPIR-V is also validated and optimized with SPIRV-Tools. - `ShaderRecompiler::Recompile` keeps compiled variants in memory, and `ShaderDiskCache` stores them on disk so later runs reuse them. Before the writable cache, a miss is also looked up in read-only prebuilt packs and loose entry folders under `shader_cache/prebuilt`; pack entries are ordinary encoded cache entries behind a sorted key-hash index, validated by magic, compiler source version and index checksum when mapped, and a miss or rejected entry falls through to the writable cache and the compiler. Cache format 35 also stores the finite stencil-reference set, its replay-safety proof and reserved push-data word in the artifact and binding layouts. Entries from formats 31 to 34 are rejected. -- The Vulkan pipelines are journaled as well ([`PipelineJournal.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp)): a graphics pipeline the store builds (`CachedPipeline`) or a compute pipeline a dispatch builds is appended to `pipelines.journal` next to the shader cache entries, as the materialized result of each stage (the specialized SPIR-V, bindings and push constants, through `ShaderDiskCache::EncodeResult`) plus the recipe the pipeline was built from (`GraphicsPipelineRecipe`: the stages' identities, vertex input layout, descriptor set layout keys and the state fields the `Pipeline` constructor reads, `CanonicalPipelineState`). When a `VulkanDevice` is created, the journal is loaded and its pipelines are built on a pool of `hardware_concurrency() / 2` threads (`APS5_PIPELINE_JOURNAL_THREADS`) while the title loads. The load parses the file a record at a time and keeps it open, and a replay thread reads an entry's modules from it when it builds that entry (a record that no longer reads back as loaded rejects the entry), so the replay holds the modules of the entries being built rather than the file, so the first draw or dispatch of a configuration seen on an earlier run finds its pipeline resident. The pipeline stores key on `RecompileResult::Identity`, a hash of the module words, specialization constants, subgroup width and bindings that is the same on every run (`SharedSpirv::ContentHash`, computed once per shared module), rather than on the process-lifetime variant ids, and a graphics pipeline is built outside the store's mutex, so the replay threads and the queue workers build in parallel. A draw that arrives before its replayed entry builds the pipeline itself; the first inserted wins. A replayed entry enters the store at its least recently used end and a full store takes none, so the title's own pipelines are never evicted for a replayed one, and the optimized relink of a replayed pipeline (`OptimizedPipeline::Schedule`) waits for the first draw that binds it, so the relink thread serves the title's pipelines first. A device created without a window starts its replay at its first draw or dispatch rather than at creation, since `Driver::AttachWindow` usually replaces it before the title draws. The journal records a pipeline once (modules and recipes are deduplicated by content), keeps at most `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024), replays the first `APS5_PIPELINE_JOURNAL_ENTRIES` recorded (1024, the pipeline store's bound, `APS5_PIPELINE_STORE_ENTRIES`; a run reaches those first), and is disabled with `APS5_NO_PIPELINE_JOURNAL=1` or without a shader cache directory. The file header names the device (`PipelineJournalDeviceFingerprint`, the properties the driver's pipeline cache file is named by), so a journal written for another GPU or driver is started over rather than replayed; appends and reads hold `pipelines.journal.lock` against another process sharing the directory, and a device cancels its replay before it is replaced (`PrepareForReplacement`) or destroyed, so a replaced headless device never inserts entries the window's device then sweeps. `APS5_PROFILE_DRAW=1` prints a `[pipeline-journal]` summary when the replay finishes and at teardown. +- The Vulkan pipelines are journaled as well ([`PipelineJournal.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp)): a graphics pipeline the store builds (`CachedPipeline`) or a compute pipeline a dispatch builds is appended to `pipelines.journal` next to the shader cache entries, as the materialized result of each stage (the specialized SPIR-V, bindings and push constants, through `ShaderDiskCache::EncodeResult`) plus the recipe the pipeline was built from (`GraphicsPipelineRecipe`: the stages' identities, vertex input layout, descriptor set layout keys and the state fields the `Pipeline` constructor reads, `CanonicalPipelineState`). When a `VulkanDevice` is created, the journal is loaded and its pipelines are built on a pool of `hardware_concurrency() / 2` threads (`APS5_PIPELINE_JOURNAL_THREADS`) while the title loads. The load parses the file a record at a time and keeps it open, and a replay thread reads an entry's modules from it when it builds that entry (a record that no longer reads back as loaded rejects the entry), so the replay holds the modules of the entries being built rather than the file, so the first draw or dispatch of a configuration seen on an earlier run finds its pipeline resident. The pipeline stores key on `RecompileResult::Identity`, a hash of the module words, specialization constants, subgroup width and bindings that is the same on every run (`SharedSpirv::ContentHash`, computed once per shared module), rather than on the process-lifetime variant ids, and a graphics pipeline is built outside the store's mutex, so the replay threads and the queue workers build in parallel. A draw that arrives before its replayed entry builds the pipeline itself; the first inserted wins. A replayed entry enters the store at its least recently used end and a full store takes none, so the title's own pipelines are never evicted for a replayed one, and the optimized relink of a replayed pipeline (`OptimizedPipeline::Schedule`) waits for the first draw that binds it, so the relink thread serves the title's pipelines first. A device created without a window starts its replay at its first draw or dispatch rather than at creation, since `Driver::AttachWindow` usually replaces it before the title draws. The journal records a pipeline once (modules and recipes are deduplicated by content), keeps at most `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024), replays the last `APS5_PIPELINE_JOURNAL_ENTRIES` recorded (4096, the pipeline store's largest bound: a replayed pipeline that finds the store full grows its bound from 1024 instead of being dropped), and is disabled with `APS5_NO_PIPELINE_JOURNAL=1` or without a shader cache directory. The file header names the device (`PipelineJournalDeviceFingerprint`, the properties the driver's pipeline cache file is named by), so a journal written for another GPU or driver is started over rather than replayed; appends and reads hold `pipelines.journal.lock` against another process sharing the directory, and a device cancels its replay before it is replaced (`PrepareForReplacement`) or destroyed, so a replaced headless device never inserts entries the window's device then sweeps. `APS5_PROFILE_DRAW=1` prints a `[pipeline-journal]` summary when the replay finishes and at teardown. - A compute shader whose LDS plus its lock dword exceeds the host's `maxComputeSharedMemorySize` is not given a `Workgroup` array. `Recompile` sets `ShaderInfo::sharedMemoryBytes` for it and the SPIR-V backend addresses the LDS in a coherent storage buffer at descriptor set 1 (`WorkgroupMemoryDescriptorSet`), with a slice of `RecompileResult::workgroupMemoryDwords` per workgroup, indexed by `WorkgroupID` and `NumWorkGroups`. `VulkanDevice` keeps one such buffer, sized from the dispatch dimensions, and binds it after the resource set; programs that fit keep using shared memory. - `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks). - A thread that needs the GPU mutex, the committer's next item or the end of the committer's queue spins before it blocks (`HandoffSpin.hpp`): `GpuMutexType::lock` retries `try_lock` for the window, the committer polls its item count, and `DrawPipeline::Drain` polls it for twice the window, so a hand-off that completes within the window costs no kernel wake-up. The window is 150 us when the process may run on more than 8 logical processors (its affinity mask, via `CpuTopology`) and 0 otherwise, where the spinning threads would compete with the holder; `APS5_HANDOFF_SPIN_US=` sets it (0 turns it off, at most 10000). Ordering and the lock's semantics are unchanged. -- Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and (with `APS5_PIPELINE_DMA=1`) DMA packets are queued as items instead. With `APS5_PIPELINE_DISPATCH=1` (off by default), a `DISPATCH_DIRECT` whose dispatch-cache entry hits with a recipe is queued as an item too, carrying the dispatch's written buffers and storage images (`DispatchWriteRanges`); the capture and the cache lookup stay on the worker and the recipe is recorded on the committer. A dispatch still drains when its lookup misses, when what it reads overlaps a write still pending, when the hit is verified against data or when its writes cannot be ranged (bindless access, GDS, fault buffer). An indirect draw resolved on the CPU (its records read by the worker) drains only when its records or its count overlap a queued write; the records it draws inline drain first, and its last drawn record is queued like a direct draw (`APS5_NO_PIPELINED_INDIRECT=1` drains at every indirect draw). An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. +- Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and DMA packets are queued as items instead. With `APS5_PIPELINE_DISPATCH=1` (off by default), a `DISPATCH_DIRECT` whose dispatch-cache entry hits with a recipe is queued as an item too, carrying the dispatch's written buffers and storage images (`DispatchWriteRanges`); the capture and the cache lookup stay on the worker and the recipe is recorded on the committer. A dispatch still drains when its lookup misses, when what it reads overlaps a write still pending, when the hit is verified against data or when its writes cannot be ranged (bindless access, GDS, fault buffer). An indirect draw resolved on the CPU (its records read by the worker) drains only when its records or its count overlap a queued write; the records it draws inline drain first, and its last drawn record is queued like a direct draw (`APS5_NO_PIPELINED_INDIRECT=1` drains at every indirect draw). An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. - A dispatch's resource build runs stage A (binding plan, buffers, descriptor set) without the GPU lock and stage B (image lookups) under it. Between them `VulkanDevice::presync` waits, still without the lock, for recorded work that writes a surface stage B would read on the CPU and for recorded work that writes the DCC keys of an image the dispatch binds (`ShaderResources::PresyncSurfaces`), so the locked `WaitForKeyWriters` finds nothing pending to synchronize with. A key range is waited for only when recorded work writes it, the key-store memo does not give its value (`DccKeyWritersPending`, without the mutex) and the pending stores do not give it either (`DccKeyWriterNeedsSync`, under the mutex, the same test `WaitForKeyWriters` makes before it synchronizes). Stage A's own key scan runs before that wait, and stage B compares it with the keys it reads under the lock as before. `APS5_NO_DCC_PRESYNC=1` leaves the key ranges out; `APS5_NO_PRESYNC=1` disables the whole pre-sync. - `APS5_DRAW_PIPELINE=1` (off by default, meant with `APS5_PIPELINED_DRAWS=32 APS5_PIPELINE_DISPATCH=1`) takes more of a draw off the worker. A `DrawFinisher` thread runs the second half of a direct pipelined draw (stage materialization, the draw cache insert and the hand-off to the committer) while the worker reads the next packets; the worker drains it before the next draw's cache lookup, before packets other than register writes and draws, and before guest reads, and finishes in place a draw whose stages may write memory (`ProgramWritesMemory`) or write through the BDA page table. The committer skips the fragment-output validation and storage-image layer scans whose inputs match its last commit (`CommitTrims`), and a resource or storage image proved current keeps its proof for the rest of the collect epoch until `GuestMemory::BreakProofs` (a CPU write, a write-back, a recorded write) moves `ProofSerial`. Color metadata passes go through the pipeline with their write ranges (`PipelinedColorMetadataPassWrites`) instead of draining it. A data-only hit whose shape entry belongs to an older draw replaces that entry in place at the second sight of the same read user words. On Linux the main image's pages get a page table entry, and capture pages are pooled across threads. `APS5_VERIFY_DRAW_PIPELINE=1` runs a full check next to 1 in 64 reused proofs and prints `[epoch-proofs]` counts. - The shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`, which also takes a hull header fused by `sceAgcFuseShaderHalves` (type 3) as the registered back half (type 7) it copies, the fused registers aside; graphics ABI resolution then uses the fused header's shader registers); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers. - The snapshot is published as pending. `SourceHandleFor`, `InvocationFor` and `ShaderPreparationTransaction::Edit` / `Read` wait for the pending job before reading `PreparedShaders`, and rethrow its failure. `APS5_SYNC_SHADER_PREPARE=1` prepares on the registering thread instead. - `APS5_LAZY_SHADER_PREPARE=1` retains registration validation and decoding but discards the temporary plan without queueing preparation. The snapshot is deferred, not pending; draws and dispatches prepare and cache it on first use. A compute or pixel stage's explicit ABI resolution (`ResolveShaderAbi`) plans the stage with the ABI's state, records the ABI and leaves its preparation to the shader's first use, which prepares it with that state (`PreparedShaders::deferredAbis`), so a shader that never draws or dispatches stays unprepared; a graphics-stage ABI is still prepared when it is resolved. This opt-in mode takes precedence over synchronous registration preparation and trades background CPU/memory use for possible first-use stalls; disk caches and shader output are unchanged. -- On devices with graphics pipeline libraries (`SpirvTarget::registrationModules`), a vertex or pixel artifact is emitted in registration form: the buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors it used to specialize on per draw are read at run time (image `ResourceMetadata` and a few state dwords in the shader data), descriptor heaps keep every element in place (an unused one gets a null or placeholder descriptor), and the pixel stage's bindings live in descriptor set 1 so each stage's library depends only on its own set layout (`INDEPENDENT_SETS`). An artifact above `registrationWordLimit` SPIR-V words is re-emitted specialized, and `CompiledShaderArtifact::registrationModule` records which form it has. For artifacts in registration form, `RegistrationResult` materializes the module once, and `PrebuildStageLibrary` builds its pre-rasterization or fragment-shader library on a separate one-thread pool (`APS5_PREBUILD_THREADS`) with a fixed default state, pairing it with a stub stage. A later draw with that state finds both libraries in the store and only links. +- On devices with graphics pipeline libraries (`SpirvTarget::registrationModules`), a vertex or pixel artifact is emitted in registration form: the buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors it used to specialize on per draw are read at run time (image `ResourceMetadata` and a few state dwords in the shader data), descriptor heaps keep every element in place (an unused one gets a null or placeholder descriptor), and the pixel stage's bindings live in descriptor set 1 so each stage's library depends only on its own set layout (`INDEPENDENT_SETS`). An artifact above `registrationWordLimit` SPIR-V words is re-emitted specialized, and `CompiledShaderArtifact::registrationModule` records which form it has. For artifacts in registration form, `RegistrationResult` materializes the module once, and `PrebuildStageLibrary` builds its pre-rasterization or fragment-shader library on a separate one-thread pool (`APS5_PREBUILD_THREADS`) with a fixed default state, pairing it with a stub stage. A later draw with that state finds both libraries in the store and only links. Ahead-of-time dispatch resolution (`resolveGroupAhead` in [`DispatchAhead.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp)) scans the upcoming dispatch run up to the next wait or synchronization packet, moves still-pending registered compute shaders to the front of the registration preparation queue (`PrioritizeRegistrationPreparation`) and queues preparation (`SubmitRegistrationPreparation`) for registered compute shaders that have no prepared entries yet (such as under `APS5_LAZY_SHADER_PREPARE=1`) with the previewed register state and entry offset; when a dispatch resolved ahead misses the dispatch cache in [`Dispatch.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp), `PrebuildComputePipeline` is submitted to the same `APS5_PREBUILD_THREADS` pool so the compute `VkPipeline` is built before the worker reaches the packet. - A draw or dispatch whose state matches no artifact prepared at registration (a title that builds its static ABI state itself, without `sceAgcDriverResolve*Abi`) gets one prepared at use by `SourceHandleFor` or `InvocationFor`, logged once per artifact and kept in the snapshot for the next use. - A stage that `sceAgcDriverResolve*Abi` cannot prepare for the state the title creates its pipeline with (a compute shader that calls user-data pointers, a descriptor the resource plan rejects) is left to its first use like a stage deferred at registration: `ResolveShaderAbi` and `PrepareGraphicsStages` catch the failure inside their own preparation transaction, so the `ShaderPreparationScope` the title's call runs in still commits, log it once per shader, mark the shader deferred and record the ABI key, so a repeated call prepares nothing; `ResolvePreparedGraphics` skips a deferred stage's rectangles until a draw has prepared it. A draw or dispatch that cannot prepare the stage either fails as before. Driver failures, `ProcessShutdown` and a failure stored by the shader's registration preparation still propagate from the ABI call. - A compute shader whose `s_swappc_b64` calls a user-data pointer cannot be linked at registration, because the callee's address is only known at dispatch. `InvocationFor` in [`ShaderRegistry.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) reads each callee from guest memory, appends one copy per call after the caller (`LinkUserDataCalls`) and prepares that linked code, so a different callee gets its own prepared entry. @@ -145,7 +145,7 @@ flowchart LR - Guest memory reaches the GPU through host imports (`VK_EXT_external_memory_host`) where the driver accepts them, and through copies otherwise ([`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). A registered range larger than the import window (64 MiB) is imported per window for buffer and fill accesses and whole for an address-based build. Live imports stay under a budget, with the least recently used import retired first (see TechnicalDebt, Host import windows and budget). - A guest protect or unmap that overlaps a plain host-pointer import retires the whole import before changing the guest range (`WaitForLeasesAndImports` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). Later accesses import surviving registered ranges again; imports never keep the pages across a cut pinned in a trimmed state. `GuestAllocations::Range::backingSerial` distinguishes the original pages from replacements: untouched pieces of a split keep their serial, newly mapped or protected pieces get a fresh one, and every span passed to `RequireUnpinned` (including the other mappings of remapped direct memory and the resident runs touched by a 2 MiB unmap or remap) gets a fresh serial when the mutation ends. - A draw binds its vertex and index ranges in place from a host import, except with NVIDIA's Windows driver, which reads a host import of guest memory far slower than a buffer it allocated (Astro Bot at Crash Site on an RTX 5070 Ti: 164 against 45 ms of draws per frame), so they are copied there (`DrawInputsInPlaceUsable`, `Context::drawInputsInPlace`). `APS5_DRAW_INPUTS_IN_PLACE=1` binds them in place and `APS5_NO_DRAW_INPUTS_IN_PLACE=1` copies them on any device; the second wins. -- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until a GPU use of it completes (a read on the settle queue ends it even while main-queue work that used the import first is still running, and the first use of an earlier import of the same pages after that read marks nothing), so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the main queue, with barriers ordering earlier and later writes around the reads. Submission does not wait on the CPU, and once the batch's fence has signalled a later lookup returns the import's whole 64 KiB blocks to the write watch (`GuestMemory::Rewatch` drops their marks and stamps every block as a CPU store), unless a live storage image overlaps them. Such an import stays compared: an image that took its generation before the stamp would take the stamp for a CPU store over the results it produces after it, and drop them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight is kept with that batch and freed once the batch has completed, without waiting for it; a guest unmap of its memory waits for the batch instead. `APS5_NO_DEFERRED_SETTLES` (any value) keeps every import under 1 MiB compared. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. On Linux the settle queue is made when the device imports dma-buf memory, and every dma-buf import of 1 MiB or more is settled right after it is made, for another reason: the kernel maps an import's pages for the GPU at the first submission that uses it, which took about 2 s for a 3 GiB import on RADV and held up the title's frame. The import's buffer is shared with the settle queue's family instead of aliased, since a sparse import is bound to many memory objects. +- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until a GPU use of it completes (a read on the settle queue ends it even while main-queue work that used the import first is still running, and the first use of an earlier import of the same pages after that read marks nothing), so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the main queue, with barriers ordering earlier and later writes around the reads. Submission does not wait on the CPU, and once the batch's fence has signalled a later lookup returns the import's pages to the write watch (`GuestMemory::Rewatch` drops the marks of every 64 KiB block they touch and stamps those blocks as a CPU store; the rest of a block the import only partly covers stays unwatched), unless a live storage image overlaps those blocks. Such an import stays compared: an image that took its generation before the stamp would take the stamp for a CPU store over the results it produces after it, and drop them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight is kept with that batch and freed once the batch has completed, without waiting for it; a guest unmap of its memory waits for the batch instead. `APS5_NO_DEFERRED_SETTLES` (any value) keeps every import under 1 MiB compared. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. On Linux the settle queue is made when the device imports dma-buf memory, and every dma-buf import of 1 MiB or more is settled right after it is made, for another reason: the kernel maps an import's pages for the GPU at the first submission that uses it, which took about 2 s for a 3 GiB import on RADV and held up the title's frame. The import's buffer is shared with the settle queue's family instead of aliased, since a sparse import is bound to many memory objects. ### Log output diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 3693189557..377205720b 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -12,7 +12,7 @@ ctest --test-dir build-relinker --output-on-failure Python must be found during CMake configuration for Python tests to be registered. List registered tests with `ctest --test-dir build-relinker -N` and look for `input_magic`, a Python-backed relinker test. If automatic discovery fails, add `-DPython3_EXECUTABLE="C:/path/to/python.exe"` to the configure command, using the path to your Python interpreter, then list the tests again. -This mode builds the conversion tool and its tests on Linux, Windows and macOS, including Apple Silicon. macOS uses AppleClang from the Xcode command-line tools. Windows uses MinGW-w64 GCC with C++20 support and accepts distributions with static or shared runtimes; the full build also requires the exception support described below. The executable is `build-relinker/core/relinker/relinker` (`relinker.exe` on Windows with Ninja). +This mode builds the conversion tool and its tests on Linux, Windows and macOS, including Apple Silicon. macOS uses AppleClang from the Xcode command-line tools (`xcode-select --install`); CMake and Ninja come from Homebrew (`brew install cmake ninja`). Windows uses MinGW-w64 GCC with C++20 support and accepts distributions with static or shared runtimes; the full build also requires the exception support described below. The executable is `build-relinker/core/relinker/relinker` (`relinker.exe` on Windows with Ninja). The output is x86-64: Linux ELF by default, Windows PE with `--windows` or Mach-O with `--macos` ([usage](../user/USAGE.md#input-and-conversion)). Converted games need system libraries built for the target OS and a compatible x86-64 host. This mode does not build those libraries, and releases ship them for Linux and Windows only. Tests inspect all three output formats; execution checks run only on their compatible hosts. @@ -195,7 +195,7 @@ On Windows use `agc_pm4_replay.exe` and prepend `build/core/libs/libs/unpatched` ## Frame capture and replay -Set `APS5_FRAME_CAPTURE` to a directory to record a few frames of a title at the AGC driver boundary, then replay them with `agc_frame_replay` through any build of the driver with no title running. From the start of the process the driver keeps every shader registration and ABI resolution, video output registration and EOP flip reservation. At the frame boundary picked by `APS5_FRAME_CAPTURE_AT_FLIP` (default 1), or by the appearance of the file `APS5_FRAME_CAPTURE_TRIGGER` names (a number in it replaces the frame count), it waits for the driver to go idle and writes every committed, readable allocation of the guest allocation registry, the title's image included, as zstd chunks without their all-zero 64 KiB blocks. A frame ends at a CPU flip or at a submission that flips (a flip packet, or the EOP release of a reserved flip token); the reservation itself only records its token. For the next `APS5_FRAME_CAPTURE_FRAMES` (10) frames it records each submission (queue, command buffer address and size), flip and suspend point with the thread that made it and the accepted and completed serials, each preceded by the guest pages the write tracker saw the CPU write since the previous call that handed work to the GPU (`GuestMemory::CollectWrittenBlocks`, per 4 KiB where the tracker kept page stamps); an allocation the registry gained after the snapshot is recorded there as a new range with its nonzero 64 KiB blocks. A submission ends a frame only when it flips itself, not when another thread's submission flipped while it was being submitted. Shader registrations and ABI resolutions only collect the blocks of the shader header and the arrays it points to, and the pages of the title's image only when their content changed. At the end it stores each display buffer the title presented, decoded, as a reference image. A capture is `calls.bin`, `memory.bin` and `capture.json` (commit, switches, sizes, warnings), and the recorder stops with a warning when its volume has less than 4 GiB free. A 20-frame capture in the Demon's Souls Nexus is 5.3 GiB of snapshot and 11.5 GiB of deltas before compression, 3.9 GiB on disk. +Set `APS5_FRAME_CAPTURE` to a directory to record a few frames of a title at the AGC driver boundary, then replay them with `agc_frame_replay` through any build of the driver with no title running. From the start of the process the driver keeps every shader registration and ABI resolution, video output registration and EOP flip reservation. At the frame boundary picked by `APS5_FRAME_CAPTURE_AT_FLIP` (default 1), or by the appearance of the file `APS5_FRAME_CAPTURE_TRIGGER` names (a number in it replaces the frame count), it waits for the driver to go idle and writes every committed, readable allocation of the guest allocation registry, the title's image included, as zstd chunks without their all-zero 64 KiB blocks. A frame ends at a CPU flip or at a submission that flips (a flip packet, or the EOP release of a reserved flip token); the reservation itself only records its token. For the next `APS5_FRAME_CAPTURE_FRAMES` (10) frames it records each submission (queue, command buffer address and size), flip and suspend point with the thread that made it and the accepted and completed serials, each preceded by the guest pages the write tracker saw the CPU write since the previous call that handed work to the GPU (`GuestMemory::CollectWrittenBlocks`, per 4 KiB where the tracker kept page stamps), read once the submissions already accepted have completed (a recorded submission, flip or suspend point reaches the driver before the recorder takes its next call, so that wait covers every call recorded before the delta); an allocation the registry gained after the snapshot is recorded there as a new range with its nonzero 64 KiB blocks. A submission ends a frame only when it flips itself, not when another thread's submission flipped while it was being submitted. Shader registrations and ABI resolutions only collect the blocks of the shader header and the arrays it points to, and the pages of the title's image only when their content changed. At the end it stores each display buffer the title presented, decoded, as a reference image. A capture is `calls.bin`, `memory.bin` and `capture.json` (commit, switches, sizes, warnings), and the recorder stops with a warning when its volume has less than 4 GiB free. A 20-frame capture in the Demon's Souls Nexus is 5.3 GiB of snapshot and 11.5 GiB of deltas before compression, 3.9 GiB on disk. `agc_frame_replay [--no-pace] [--no-release] [--release-after-ms ] [--no-synthesize] [--strict] [--dump
::]... [--frames ]` maps the captured ranges at their addresses and registers them with their access (a range the replaying process already maps writable, such as the driver's fixed memory, is only refilled, and a non-GPU range it uses itself is skipped and counted), repeats the registrations, then replays the calls of each captured thread on a thread of its own, started in captured order. Before a call that hands work to the GPU it waits until no more submissions are in flight than when the title made it (`--no-pace` turns this off). Titles release GPU waits from threads that never call the driver, so when the driver completes nothing for 3 ms the replay reads the waits its queues are blocked on (`AgcDriverAwaitedWaits`) and writes the next recorded CPU value of the awaited dword that satisfies the compare, else the last earlier one, else a value derived from the reference (`--no-synthesize` turns this last step off). It prints one JSON line with the frame count and times, the submissions, the pacing wait, the released waits and how many were derived, the submissions that never completed and, for captures with reference images, the share of identical pixels and the largest channel difference of the replayed display buffers (`--frames` writes both images). Effects that depend on timing (temporal antialiasing, particles) make pixels differ, so expect a visual match rather than identical frames. Replay times follow the GPU clock state: lock the clocks (`nvidia-smi -lgc`, `-lmc`) when comparing two drivers. @@ -216,11 +216,11 @@ On a device that supports `VK_EXT_device_fault`, the first `VK_ERROR_DEVICE_LOST ## Worker sampler -Set `APS5_SAMPLE_WORKER=` to sample queue worker 0 every millisecond (in practice every timer tick). Every 20 s and at exit it writes ``, one `module offset self inclusive` line per sampled address, and `.collapsed.txt`, the worker's whole stacks in the collapsed format that speedscope.app and flamegraph.pl read: one `frame;...;frame count` line per distinct stack, root first, each frame as `module+0xRVA`. A walk that ends before the thread's root starts with `[truncated]`. The `#` lines give the samples, the walk depth (64 frames), the truncated walks, the mean and longest time the worker was suspended, and the size and PE timestamp of each module, to check the binary a file is symbolized against. A frame with `.pdata` is stepped by `RtlVirtualUnwind`; any other frame by its DWARF record in `.ehfram`, the unwind data the libraries are built with. Those steps read only inside the worker's stack, and a module is indexed after the worker resumes, the first time a walk meets it. +Set `APS5_SAMPLE_WORKER=` to sample queue worker 0 every millisecond (in practice every timer tick). Every 20 s and at exit it writes ``, one `module offset self inclusive` line per sampled address, and `.collapsed.txt`, the worker's whole stacks in the collapsed format that speedscope.app and flamegraph.pl read: one `frame;...;frame count` line per distinct stack, root first, each frame as `module+0xRVA`. A walk that ends before the thread's root starts with `[truncated]`. The `#` lines give the samples, the walk depth (64 frames), the truncated walks, the mean and longest time the worker was suspended, and the size and PE timestamp of each module, to check the binary a file is symbolized against. A frame with `.pdata` is stepped by `RtlVirtualUnwind`; any other frame by its DWARF record in `.ehfram`, the unwind data the libraries are built with. Those steps read only inside the worker's stack, and a module is indexed after the worker resumes, the first time a walk meets it. `python tools/symbolize_samples.py .collapsed.txt --libs ` names each frame from the COFF symbols of the module that ran, or from its export table when it has none, and leaves a module unsymbolized when its size or timestamp differs from its `# module` line or when it has none (`--unverified` symbolizes it anyway); `--since ` keeps only the samples taken after that copy, `--out` writes the named stacks and `--demangle` uses `c++filt`. Given ``, it lists functions by self and by inclusive samples, where a function's inclusive count is the sum of its addresses' and overcounts a function that is on one stack more than once. `APS5_SAMPLE_COMMITTER=` samples queue 0's draw committer the same way and writes the same two files. The committer records the draws the worker hands over (`APS5_PIPELINED_DRAWS`), so when the worker's stacks wait in `DrawPipeline::Enqueue`, its stacks show what bounds the frame. It starts with the first pipelined draw; with `APS5_PIPELINED_DRAWS=0` nothing is sampled. -`APS5_SAMPLE_THREADS=` samples every thread of the process every 2 ms and writes the 40 hottest addresses of each busy thread to ``, 12 frames deep and through `.pdata` only. The samplers exist only on Windows; the addresses are symbolized offline, for example with `nm` on the `.prx`. +`APS5_SAMPLE_THREADS=` samples every thread of the process every 2 ms and writes the 40 hottest addresses of each busy thread to ``, 12 frames deep and through `.pdata` only, under a `thread samples ` line per thread and with the same `# module` lines. The samplers exist only on Windows; the addresses are symbolized offline with `tools/symbolize_samples.py`, which adds up all threads or, with `--thread `, keeps one. ## Performance trace @@ -248,7 +248,7 @@ The switch combines with `APS5_TRACE_FRAMES` or `APS5_TRACE_SECONDS`. The interv Set `APS5_TRACE_BENCH=1` together with `APS5_TRACE_SUMMARY_EVERY` to also keep every interval's frame times for a benchmark report. At each interval the presenter hands the kept intervals to a writer thread, which formats and compresses the file and rewrites `aps5-bench--.json.gz` in `APS5_TRACE_DIR` or the working directory: one JSON object with `format` (`aps5-bench/1`), the identity fields of the summary line (`commit`, `dirty`, `env`, `gpu`, `driver`, `os`, `cpu`, `cpu_threads`, `memory_mib` and, with `param.json`, `title_id`, `title` and `content_version`), `intervals_dropped` and `intervals`. Each interval has `start_s`, `end_s`, `first_present`, `resolution`, `gpu_busy_ms` (the interval's total) and `frame_us`, the whole microseconds between consecutive presents. The file keeps the last 240 intervals; `intervals_dropped` counts older ones it no longer holds. Any other value, or the switch without `APS5_TRACE_SUMMARY_EVERY`, fails the first present. The file is written to `.tmp` and then moved over the previous one, so a crash or kill during a rewrite leaves the last complete report. Only the newest report waits for the writer: one handed over while an older one is still pending replaces it. The frame times cost 4 bytes per present in memory. -`python tools/trace_digest.py aps5-summary.jsonl` prints the interval lines of such a file as one row per interval (frame p50, p90, p99, max, GPU busy per present, the busiest queue worker and the largest phase) and a text sparkline of the frame p99 and of the GPU busy time; lines of window captures in the file are skipped, and a change of commit, environment, GPU, driver or OS between intervals is reported. `--json` prints the rows as JSON. +`python tools/trace_digest.py aps5-summary.jsonl` prints the interval lines of such a file as one row per interval (frame p50, p90, p99, max, GPU busy per present, the busiest queue worker and the largest phase) and a text sparkline of the frame p99 and of the GPU busy time; lines of window captures in the file are skipped, and changes to build, environment, host, resolution or title metadata between intervals are reported. `--json` prints the rows, the first interval's identity and the indices of identity changes as JSON. With `APS5_TRACE_SUMMARY_EVERY` set, the recorder writes a timestamp pair per batch for the whole run and every `PerformanceTimer` scope adds its time to the thread's totals, two clock reads per scope. A thread's totals are read while it runs, so a scope that straddles an interval boundary is counted in one interval or the other. diff --git a/docs/dev/CONVENTIONS.md b/docs/dev/CONVENTIONS.md index 87c674dac4..fdf0b83768 100644 --- a/docs/dev/CONVENTIONS.md +++ b/docs/dev/CONVENTIONS.md @@ -20,7 +20,7 @@ There's no need to add image files to the repository. You can add images to the Host code that writes into guest memory through the kernel or a native library must hold a `GuestArena::HostWrite` ([GuestArena.hpp](../../core/libs/prx/libc/include/GuestArena.hpp)) over the destination range for the whole write. This covers file reads (`ReadFile`, `read`, `pread`), socket receives, `DeviceIoControl` and other calls that fill a caller's buffer. -The write watch can leave guest pages read-only between collections: every watched page on macOS, and stable resident runs of at least 256 KiB on Windows (see the write-watch entries in [debt/libc.md](debt/libc.md)). A kernel-mode write into such a page doesn't fault into the watch. It fails instead, with `EFAULT` on macOS and `ERROR_NOACCESS` on Windows. `HostWrite` opens the pages and makes sure the write is recorded. +The write watch can leave guest pages read-only between collections: every watched page on macOS, and on Windows the pages of shared views (shared memory, shared file mappings, and direct memory mapped at more than one guest address), which it tracks by write protection (see the write-watch and Windows direct-memory backing entries in [debt/libc.md](debt/libc.md)). A kernel-mode write into such a page doesn't fault into the watch. It fails instead, with `EFAULT` on macOS and `ERROR_NOACCESS` on Windows. `HostWrite` opens the pages and makes sure the write is recorded. - A write that completes later on another thread (overlapped I/O, completion ports, asynchronous reads) is bracketed where the bytes land, not where the request was issued. - Ordinary CPU stores from host C++ code need no bracket: the write watch records them (through a fault where the page is protected). diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 3a728911cd..6beedd592e 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -10,6 +10,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `ANYPS5_JOB_MANAGER_HOST` | presence | `core/libs/tests/JobManagerReference.cpp` | | runs the reference adapter against the host fallback instead of the loaded guest module | | `ANYPS5_LOADEXEC_PARENT` | string | `core/libs/tests/GuestLoadExecHost.cpp` | | variable the Windows entry stub reads; its wide name is built in `core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp` | | `ANYPS5_LOADEXEC_TEST_ROOT` | string | `core/libs/tests/GuestLoadExecHost.cpp` | | directory the load-exec host test reads its guest executable from | +| `ANYPS5_POSIX_SPAWN_ROLE` | string | `core/libs/tests/GuestPosixSpawn.cpp` | | role of the test's own copy spawned as a `posix_spawn` child, which exits or sleeps on it and otherwise fails | | `ANYPS5_TEST_QUOTED_ARGV0` | presence | `core/libs/tests/GuestLoadExecHost.cpp` | | makes the load-exec test's first argument one with quotes, a backslash and non-ASCII text | | `APS5_EBOOT` | string | `core/libs/tests/GuestSystemExit.cpp` | | host path of the executable launched for /app0/eboot.bin, defaulting to the running executable | | `APS5_SKIP_TIMING_TESTS` | presence | `core/libs/tests/PreciseWait.cpp` | | treats the timing tests' runner as preempted, so they skip without probing the clock | @@ -39,7 +40,9 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_CODE_HASH_KEY` | presence | `core/shader/recompiler/CacheKey.hpp` | | appends every code word to the cache key instead of hashing the code | | `APS5_NO_FAILURE_MEMO` | presence | `core/shader/recompiler/Recompiler.cpp` | | rebuilds a program whose compilation failed instead of reusing the memoized failure | | `APS5_NO_IMAGE_TABLES` | presence | `core/shader/recompiler/Optimization/src/ResourceTracker.cpp` | | disables image tables, leaving table-loaded descriptors unplanned | +| `APS5_NO_MASKED_RESULT_MEMO` | presence | `core/shader/recompiler/Recompiler.cpp` | | materializes every result memo miss instead of patching the buffer descriptors, user data and flattened SRT of a cached result whose snapshot differs only in those words | | `APS5_NO_PERF_DISK_PLANS` | presence | `core/shader/recompiler/Recompiler.cpp` | | disables the disk cache of planning results | +| `APS5_NO_PERF_DOMINATOR_TREE` | presence | `core/shader/recompiler/ControlFlow/src/Structurizer.cpp` | | computes the structurizer's dominator and post-dominator sets with the set fixpoint instead of a dominator tree, walks every selection construct instead of testing its members by dominance, and tests every block against each construct when checking the structured constructs instead of walking the construct | | `APS5_NO_PERF_DOM_INTERSECTION` | presence | `core/shader/recompiler/ControlFlow/src/Structurizer.cpp` | | computes dominator intersections into a fresh vector instead of intersecting in place | | `APS5_NO_PERF_ITERATION_SELECTION` | presence | `core/shader/recompiler/ControlFlow/src/Structurizer.cpp` | | recomputes the post-dominator sets when finding a loop's iteration-selection merge instead of reusing them | | `APS5_NO_RESULT_MEMO` | presence | `core/shader/recompiler/Recompiler.cpp` | | materializes a fresh result for every recompile instead of reusing the result memo | @@ -49,12 +52,14 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_WINDOW_NARROWING` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp` | | indexes every slot of the shader's vector window instead of only the slots the program uses | | `APS5_PROBE` | string | `core/shader/recompiler/Translation/src/DispatchInstructions.cpp` | | pc:vgpr[:shift] probe that copies that VGPR into v255 after the instruction so image stores write it as a per-lane trace | | `APS5_RAYTRACING` | string | `core/shader/recompiler/Translation/src/DispatchInstructions.cpp` | | selects the image_bvh_intersect_ray behaviour: strict throws and miss writes 0xffffffff | +| `APS5_RESULT_MEMO_MB` | integer | `core/shader/recompiler/Recompiler.cpp` | 256 | MiB the result memos of all shader sources may hold before the least recently used results are dropped (0 keeps them until their set evicts them) | | `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_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_WAVE_LDS_SUBGROUP` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp` | | restores subgroup-scope LDS barriers for single-wave workgroups instead of workgroup scope | @@ -63,7 +68,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| -| `APS5_TRACE_LABEL` | integer | `core/libs/prx/libSceAgc/Cb/src/Sync.cpp` | 0 | hex guest address of a RELEASE_MEM label whose packet is logged when recorded | +| `APS5_TRACE_LABEL` | integer | `core/libs/prx/libSceAgc/Cb/src/Sync.cpp`, `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp` | 0 | hex guest address; logs a matching RELEASE_MEM when recorded, and nearby RELEASE_MEM, WRITE_DATA, COPY_DATA, DMA_DATA and WAIT_REG_MEM packets when executed | ### libSceAgcDriver @@ -217,6 +222,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_CPU_COPY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | | disables the CPU copy path of the copy HLE | | `APS5_NO_DATA_HITS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | | disables dispatch-cache hits that differ only in ShaderData or FlattenedSrt words | | `APS5_NO_DCC_PRESYNC` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | leaves the DCC key ranges out of the surfaces a build pre-syncs, so only the surface itself waits for its producer | +| `APS5_NO_DEFERRED_DESTRUCTION` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | on MoltenVK, destroys buffers and frees device memory as soon as the driver releases them, instead of while the graphics queue is idle (KhronosGroup/MoltenVK#2876) | | `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) | @@ -347,15 +353,14 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_PACKET_EPOCH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitStatistics.cpp` | | makes queue workers bump the collect epoch before every packet | | `APS5_PIPELINED_DRAWS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp` | 8 | depth of the queue-0 draw pipeline: 0 records draws inline, 1 selects the default depth of 8, and any other value is the depth capped at 256 | | `APS5_PIPELINE_DISPATCH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | records an eligible dispatch as an item in the queue-0 pipeline instead of running it inline | -| `APS5_PIPELINE_DMA` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | records a queue-0 DMA data packet in the draw pipeline instead of executing it inline | | `APS5_PIPELINE_DRAIN_FILLS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp` | | runs queue-0 buffer fills through the draw pipeline to keep their order; any value stores them in place instead | | `APS5_PIPELINE_DRAIN_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | answers a queue-0 wait from the draw pipeline's own pending labels while it is busy; any value drains the pipeline at the wait | | `APS5_PIPELINE_DRAIN_SUBMISSIONS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | lets a submission end without submitting its recorded work, so it joins a later submit with the pipeline's queue; any value drains the pipeline and submits at the end of the submission | -| `APS5_PIPELINE_JOURNAL_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp` | 1024 | number of journal entries loaded at most; a longer journal keeps its last entries, the ones recorded last | +| `APS5_PIPELINE_JOURNAL_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp` | 4096 | number of journal entries loaded at most; a longer journal keeps its last entries, the ones recorded last | | `APS5_PIPELINE_JOURNAL_MAX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp` | 1024 | size cap in MiB on the pipeline journal; the file is rewritten down to three quarters of the cap when it is reached, keeping the newest pipelines and the modules they name and leaving room for later records | | `APS5_PIPELINE_JOURNAL_THREADS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp` | | threads that replay the journal's pipelines, 0 disables the replay; unset uses half the hardware threads, at least one | | `APS5_PIPELINE_STATS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | captures and prints driver statistics for each new pipeline, requiring `VK_KHR_pipeline_executable_properties`; `1` enables | -| `APS5_PIPELINE_STORE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp` | 1024 | LRU bound on the entries of the pipeline store; an entry is evicted only once no recorded draw holds it | +| `APS5_PIPELINE_STORE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp` | adaptive | fixes the LRU bound on the entries of the pipeline store, which otherwise starts at 1024 and doubles up to 4096 for a journal replay that does not fit and while evicted pipelines keep coming back; an entry is evicted only once no recorded draw holds it | | `APS5_PM4_RING` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp` | 16 | number of submissions the PM4 ring keeps for dumps, from 0 to 4096; 0 disables the ring | | `APS5_POLL_REAP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | reaps completions on every poll tick | | `APS5_POLL_TRY_EACH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | tries the poll service on every tick instead of at the poll interval | @@ -379,7 +384,8 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SKIP_INVALID_INDIRECT_REGISTERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | skips the entries whose offset dword is no register offset in a SET_*_REG_INDIRECT list read again where its packet executes, and writes its valid entries, instead of stopping | | `APS5_SKIP_REFUSED_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | | skips a draw whose graphics stage the recompiler refuses, reporting each refused shader once, instead of failing the queue | | `APS5_SKIP_REQUEST_DUMPS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp` | 8 | caps RecompileRequest dumps per shader-skip reason | -| `APS5_SKIP_SHADER` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | 0 | hex program address of a shader to skip preparing | +| `APS5_SKIP_SHADER` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | 0 | hex program address of a shader whose draws and dispatches are dropped | +| `APS5_SKIP_SHADER_FILE` | path | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | | file of whitespace-separated hex program addresses (`#` starts a comment) whose draws and dispatches are dropped; re-read within 500 ms of a change; a token that is not a whole 64-bit hex number is reported and ignored | | `APS5_SNAPSHOT_CHECK_FATAL` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | throws on the first snapshot that differs from registered memory instead of only counting them | | `APS5_SPANNED_WAVE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | spans a single-wave wave64 compute program over two full 32-lane subgroups | | `APS5_SPIRV_OPT` | string | `core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp` | | selects the SPIR-V optimization pipeline: `full` restores the performance pipeline, `none` skips optimization | @@ -405,7 +411,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_TEMPLATE_AUDIT_MS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 1000 | milliseconds after which a cached shader-resource object takes the full walk instead of the fast proof; `0` turns the audit off | | `APS5_TEXTURE_CACHE_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 0 | forced byte budget in MiB for the storage and sampled texture caches; `0` selects the budget automatically | | `APS5_THREAD_NICE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | -10 | nice value the raised queue workers and presenter ask rtkit for (clamped to -20..19) | -| `APS5_THREAD_PRIORITY` | string | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | off | raises the queue workers and presenter above the title's threads: `off`, `high` or `rt` | +| `APS5_THREAD_PRIORITY` | string | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | off | raises the queue workers and presenter above the title's threads: `off`, `high` or `rt`; on macOS `high` is the user-interactive QoS class and `rt` a Mach time-constraint policy (period 1 ms, computation 0.5 ms) | | `APS5_THREAD_RT` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | 10 | realtime priority the raised threads ask rtkit for (clamped to 1..99) | | `APS5_THREAD_RTTIME_US` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | 200000 | hard RLIMIT_RTTIME the realtime plan lowers to, in microseconds | | `APS5_TRACE_BARRIERS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | prints that many simulated hazard barriers before stopping | @@ -565,6 +571,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `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_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 | | `APS5_WATCH_ADDR` | integer | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | absolute hex address whose writes are reported after its page is committed | diff --git a/docs/dev/GAME_TESTING.md b/docs/dev/GAME_TESTING.md index 6e74085f0e..de2c53b6f3 100644 --- a/docs/dev/GAME_TESTING.md +++ b/docs/dev/GAME_TESTING.md @@ -12,9 +12,12 @@ For user options see [USAGE](../user/USAGE.md); for internals see [ARCHITECTURE] - Linux: `relinker --registry src/eboot.elf app.elf` - Windows: `relinker --windows --registry src/eboot.elf app.exe`. Without `--windows` the output is an ELF file whatever its name, and Windows refuses to start it (exit code 216, "not compatible with the version of Windows"). - A dump whose `app0` holds `ampr_assets.index` and `ampr_assets-*.pak` was repacked for [ampr_emu](https://github.com/drakmor/ampr_emu): the packed files were removed from `app0`, and only ampr_emu reads the packs. Restore them first with ampr_emu's `tools/ampr_pack.py unpack --index app0/ampr_assets.index --output ` and copy the files missing from `app0`. Without them a Unity title stops at start-up on the missing `Media/Resources/unity default resources`. -- Convert every module the title loads from its own folders (`sce_module/`, `Media/Modules/`, `Media/Plugins/`). Unity titles need `--module-dir Media/Modules --module-dir Media/Plugins`. Put each printed `Guest module:` file next to its original `.prx` in `app0/`, and keep the original there. +- Convert every module the title loads from its own folders (`sce_module/`, `Media/Modules/`, `Media/Plugins/`). Unity titles need `--module-dir Media/Modules --module-dir Media/Plugins`. The relinker writes each converted module as `.guest.prx` into `app0/` beside the output, in the module's own folder, and lists it in the `Arrange the files like this` tree it prints. Keep the original `.prx` next to it. - `libs/` holds only AnyPS5's own libraries, from `build/core/libs/libs` (not `unpatched/`). A game module placed there fails to load (error 193 on Windows). A self-built `libs/` on Windows also needs `libgcc_s_seh-1.dll`, `libstdc++-6.dll` and `libwinpthread-1.dll` from the toolchain's `mingw64/bin`, or every library fails with error 126 ([USAGE](../user/USAGE.md#input-and-conversion)). - Run `tools/import_audit.py` on the registries. Exit code 1 lists imports that will fail or throw. + `tools/audit_dump.py --relinker --libs build/core/libs/libs` (`--windows` on Windows) does the + unwrapping, the conversion with `--registry` and the audit in one step, from every module folder above and `.prx` + files beside `eboot.bin`, into a temporary directory (or `--work `), reading the dump only. - Run one title at a time. Titles share the GPU and the controller file. Some titles start a second process (Peace Walker's `mgspw.mgspw.elf`) that can outlive a time cap on the first: check that every process of the previous run is gone. ## Launch diff --git a/docs/dev/TRACING.md b/docs/dev/TRACING.md index 8cb84bb4a4..51854e22c3 100644 --- a/docs/dev/TRACING.md +++ b/docs/dev/TRACING.md @@ -11,7 +11,8 @@ Set a switch before starting the emulator. Unless noted, any value enables it. M | `APS5_TRACE_APR` | APR file reads, including path, offset, size and destination. | | `APS5_TRACE_AUDIOOUT` | Audio output port parameters when a port is opened. | | `APS5_TRACE_AUDIOOUT2` | AudioOut2 context and port parameters, attributes, pushes, queue polls and periodic queue summaries. | -| `APS5_TRACE_CALLS` | On Windows and Linux, records calls to the imports selected by this comma-separated library list (or `all`); crash and abort reports (abort only on Linux) include the last 64 calls and most-called imports. The executable must be converted with `--trace-imports`; see [Usage](../user/USAGE.md#tracing-system-library-calls). | +| `APS5_TRACE_CALLS` | On Windows, Linux and macOS, records calls to the imports selected by this comma-separated library list (or `all`); crash and abort reports (abort only on Linux and macOS) include the last 64 calls and most-called imports. The executable must be converted with `--trace-imports`; see [Usage](../user/USAGE.md#tracing-system-library-calls). | +| `APS5_TRACE_CALLS_FILE` | With `APS5_TRACE_CALLS`, a file rewritten every 10 seconds with every traced import called so far, most called first: number of calls, library and import name. | | `APS5_TRACE_EXIT` | Guest `exit` and `_Exit` calls and process-exit backtraces. | | `APS5_TRACE_FIBER` | Fiber initialization, switches and returns. | | `APS5_TRACE_MEMORY` | Direct-memory allocation, mapping and protection operations. | @@ -45,7 +46,7 @@ Set a switch before starting the emulator. Unless noted, any value enables it. M | `APS5_TRACE_HOST_IMPORT` | Host-import mapping attempts, memory-map details, failures and retirement. | | `APS5_TRACE_IMAGE_TABLE` | Image-table resource materialization and descriptor selection. | | `APS5_TRACE_INDIRECT_DRAWS` | Indirect draw packets that pass the draw precheck. | -| `APS5_TRACE_LABEL` | `RELEASE_MEM` packets that target the address in this hexadecimal value. | +| `APS5_TRACE_LABEL` | A matching `RELEASE_MEM` when recorded, and nearby `RELEASE_MEM`, `WRITE_DATA`, `COPY_DATA`, `DMA_DATA` and `WAIT_REG_MEM` packets when executed. Set the value to the guest address in hexadecimal. | | `APS5_TRACE_LATE_LABELS` | GPU label writes observed after their waiting submission was handled. | | `APS5_TRACE_QUERY` | Every sixteenth guest-memory page query, including cost and caller address. | | `APS5_TRACE_RESOURCE_KEY_MISSES` | Resource-content key changes that cause draw or dispatch resource-cache misses. | diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index ee2a2dfd75..a9bed3b21f 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -29,3 +29,4 @@ Throughout the project, every function at every stage either **does exactly what - `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. - [Quick syntax checks](../../tools/check_compile.py) reject unexpanded `@response` arguments rather than parse compiler-specific quoting and risk retaining output flags. Expand response files in `compile_commands.json` before checking; the Ninja fallback expands them with `ninja -t compdb -x`. +- [Quick syntax checks](../../tools/check_compile.py) do not model every compiler option's operand count. A preprocessing mode following an unrecognized option without a joined value is rejected rather than risk removing that option's operand. Compiler wrappers with their own option grammar are not covered. Forwarded frontend actions (`-Xclang`, `-Xpreprocessor` and their joined forms) are rejected. Comma-forwarded options (`-Wp,`) allow only recognized macro/include operands; response-file tokens are rejected even as operands, and other options are rejected because unmodeled actions or output operands could bypass syntax analysis or write outputs. See the [Clang option grammar](https://clang.llvm.org/docs/ClangCommandLineReference.html). diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index aa30dca0b0..bcd29e2c3f 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -15,10 +15,10 @@ - [sceAgcAcbAtomicGds_0900](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - signature, control/mode bit mapping and the 11-dword ATOMIC_GDS (0x1d) layout follow the clean-room [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, which derives them from the library's disassembly; KytyPS5 and shadPS4 have no body for it. Not confirmed on hardware or a title (PPSA07286 imports it, nothing was run), and the driver has no ATOMIC_GDS handler (opcode 0x1d is not executed as an atomic GDS operation) - [sceAgcGetIsTrinityMode](../../../core/libs/prx/libSceAgc/Misc/src/Platform.cpp) (libSceAgc) - signature from the only PPSA26344 call (a pointer to a one-byte flag, result ignored); a null pointer throws because its error code is unknown - [sceAgcDcbAtomicGds_0900](../../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - and `sceAgcDcbAtomicGds` (forwards to it) - signature, control/mode bit mapping and the 11-dword ATOMIC_GDS (0x1d) layout are those of `sceAgcAcbAtomicGds_0900`, which follow the clean-room [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only; KytyPS5 and shadPS4 have no body for it. Not confirmed on hardware or a title, and the driver has no ATOMIC_GDS handler -- [sceAgcDcbContextStateOpGetSize](../../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - sizes from KytyPS5 only (5, 27, 27 and 32 dwords), equal to what `sceAgcDcbContextStateOp_0100` writes; KytyPS5 returns 0 for an operation above 3, here it throws - [sceAgcDcbContextStateOp](../../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - delegates to `sceAgcDcbContextStateOp_0100` (same packets as KytyPS5's `AgcDcbContextStateOp`); KytyPS5 returns nullptr for an operation above 3, here it throws; the packet words were not compared with a real console - [sceAgcBranchPatchSetThenTarget_0300, sceAgcBranchPatchSetElseTarget_0300](../../../core/libs/prx/libSceAgc/Patch/src/Branch.cpp) (libSceAgc) - names from their NIDs (`7Wa3aeJgeVU`, `rP5xLdOf26k`); the `(packet, cachePolicy, target, sizeInDwords)` signature is inferred from PPSA28416's calls (a branch packet, 0, an aligned target, 4), with the policy stored where `sceAgcCbBranch` puts it - [sceAgcDcbSet{Cx,Sh,Uc}RegistersIndirectGetSize](../../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - the count argument is ignored: PPSA28416 calls them with leftover register contents (0xf5f5039c), so the real functions likely take none; the size is 20 bytes either way +- [sceAgcCbSetShRegistersDirect, sceAgcCbSetUcRegistersDirect](../../../core/libs/prx/libSceAgc/Cb/src/Registers.cpp) (libSceAgc) - a register list may start at any byte address, and its 8-byte `{offset, value}` entries are copied byte for byte. PPSA26344 (02.002.000) passes the `sh_registers` lists of compute shader headers that sit 1 byte past an 8-byte boundary in `core_common_subcore.core_b1_fx.sprig.xpps` ([#902](https://github.com/boykopovar/AnyPS5/pull/902)), so those lists start at 1 mod 4 (0x8021363131 in an open-world run). The title runs on the console with these lists, but how the console reads an unaligned list (at the given address, or at a rounded-down one) is not measured - [sceAgcUpdateInterpolantMapping_0100](../../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - name from its NID (`k0E7vkgqAuE`); the `(regs, vs, ps)` arguments are the declared ones, behaviour unknown - [sceAgcWriteDataPatchSetDst, sceAgcWriteDataPatchSetCachePolicy and their Async forms](../../../core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp) (libSceAgc) - the signatures `(packet, uint8 value)` are inferred from the other patch functions, as no public declaration was found; the value is taken in the encoding of the `dst` and `cachePolicy` arguments of `sceAgcDcbWriteData` (graphics) and `sceAgcAcbWriteData` (Async, compute), and the patched packet is the one those calls would write. `sceAgcAsyncWriteDataPatchSetAddressOrOffset` is assumed to patch the address like `sceAgcWriteDataPatchSetAddressOrOffset`, as the compute WRITE_DATA packet keeps it in the same dwords - [sceAgcCreateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - name from its NID (`dbOlWdppb4o`); same arguments as `sceAgcCreateInterpolantMapping_0100`; PPSA26344 passes three pointers at each of nine observed call sites (`0x9F914E`, `0x9F91E2`, `0x9F9216`, `0x1604BBD`, `0x16122D7`, `0x1618285`, `0x161838C`, `0x16184EE`, `0x1618607`), consistent with that argument assumption, but does not read the return value there, so its return type is not established. @@ -43,8 +43,9 @@ - [sceAgcSetAmmSemaphoreMemory, sceAgcSetSemaphoreMemory, sceAgcGetSemaphoreLabel](../../../core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp) (libSceAgc) - the checks, error codes and clearing are the firmware's, but two firmware steps are not reproduced. After its checks, the firmware validates the memory with libkernel `qSleeVC6lwI`: an ioctl `0xc018ac15` taking `{memory, size, 1}`. The AMM call always does this; the non-AMM call only for memory from 8 TiB up that ends below 15 TiB. The firmware then sends `{7: memory, 8: size, 0xb: 0 for AMM, 1 otherwise}` through sceAgcDriverPassInfoDownward. Neither step happens here, so a registration that the kernel or driver would refuse succeeds. A null memory with a non-zero size and a range that wraps the address space throw where the firmware would fault - [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) - the DCB calls write the packets [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/agc.cpp) writes: an 18-dword (active) or 12-dword (complete) `IT_NOP` packet with the stream id, and for active the 64-bit mask of the workload ids, for complete the workload id and the 64-bit clear mask of that id. Both return null for a null command buffer, an unregistered or out-of-range stream, a workload id above 63, and for active a duplicate id or a count of 0 or above 63. The layouts and checks come from KytyPS5 alone; the console oracle in [prosper](https://github.com/mattias800/prosper/blob/main/prosper/tests/data/console_oracle/agcbuild.golden.tsv) measures the null return only for unregistered streams, not the packet contents of a registered one. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. - [sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - return null without writing to the buffer for a null command buffer or a stream that is not registered (out-of-range ids included), and throw for a registered stream. The console oracle in prosper measured only that null return, with no stream registered, three argument patterns each and nothing written ([ACB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L48-L56), [DCB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L196-L204)); the packet a registered stream gets is not measured. KytyPS5 has no body for these four, and its `IT_NOP` layout for the DCB active and complete calls differs from the SET_UCONFIG_REG, WRITE_DATA and SET_WORKLOAD sequence [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/agc_workload_packet.c) derives from the driver, so neither is taken. The `(buf, streamId)`, `(buf, streamId, workloadId)` and `(buf, streamId, workloadIds, workloadCount)` signatures are those of the DCB calls in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L2317-L2384), which says the ACB calls use the same driver builders; no ACB declaration was found. `sceAgcDcbSetWorkloadsActive` checks the stream before it reads the workload ids, as OpenAGC does: the console returned null for stream 1 with the ids at address 2 ([measured](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L203)) without faulting -- [sceAgcDcbSetWorkload*](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkload*](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - the console's packets are not written: the executor has no ATOMIC_MEM or mask table. In `libSceAgc.sprx` (firmware 12.00, read from disassembly) all six write a SET_UCONFIG_REG to 0x342 (tag `0xCC000000 | stream`, or `0xCD000000 + workload * 32 + stream` for complete) and a WRITE_DATA to 0xC343, and active and complete add an ATOMIC_MEM (header `0xC0071E00`) on the stream's 64-bit mask. [Measured on a PS5](https://gist.github.com/orezraey/befe7632b827421988b47a6c04e89e1d) (firmware 13.60): WRITE_DATA dwords 5 to 8 are `0x06010000`, `0xC343`, 0 and the mask's high half (0 for inactive); the header low byte is 0x04 on the DCB SET_UCONFIG_REG and WRITE_DATA and 0x00 on the ACB ones; the table was at `0xFE003A000 + 8 * stream`; the pointer passed to `sceAgcDriverRegisterWorkloadStream` is never written +- [sceAgcDcbSetWorkload*](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkload*](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - the console's packets are not written: the executor has no ATOMIC_MEM or mask table. In `libSceAgc.sprx` (firmware 13.60, read from disassembly) all six write a SET_UCONFIG_REG to 0x342 (tag `0xCC000000 | stream`, or `0xCD000000 + workload * 32 + stream` for complete) and a WRITE_DATA to 0xC343, and active and complete add an ATOMIC_MEM (header `0xC0071E00`) on the stream's 64-bit mask. [Measured on a PS5](https://gist.github.com/orezraey/befe7632b827421988b47a6c04e89e1d) (firmware 13.60): WRITE_DATA dwords 5 to 8 are `0x06010000`, `0xC343`, 0 and the mask's high half (0 for inactive); the header low byte is 0x04 on the DCB SET_UCONFIG_REG and WRITE_DATA and 0x00 on the ACB ones; the table was at `0xFE003A000 + 8 * stream`; the pointer passed to `sceAgcDriverRegisterWorkloadStream` is never written - [sceAgcCreatePrimState](../../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well - [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) (libSceAgc) - non-sample-dump event types (any event other than 0x38 and 0x39) emit a 2-dword PM4 packet 0x46 without address dwords; the address argument is ignored for these events. Void Terrarium (PPSA03061, #5055) passes a non-null address buffer (0x00000002d2a10000) with event type 0x16 (cache flush). - This pending declaration has an unverified signature and behavior; it is a parameterless import-resolution declaration that throws through `NotImplemented_nid_no_patch`: [`sceAgcAcbInitializeDefaultHardwareState`](../../../core/libs/prx/libSceAgc/Unimplemented.cpp). +- [sceAgcAcquireMemSetEngine, sceAgcCbMemsetExclusive](../../../core/libs/prx/libSceAgc/Unimplemented.cpp) (libSceAgc) - declared with canonical signatures from decrypted headers; behavior on retail consoles remains unmeasured. Both throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 36535fa9b4..bf768f57f8 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -9,9 +9,10 @@ - [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 -- [sceAgcDriverSubmitAcb, sceAgcDriverSubmitMultiAcbs](../../../core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp) (libSceAgcDriver) accept compute queues 0x20 to 0x57 and throw for any other. On a PS5 (firmware 13.40, measured 2026-10-19 from inside a retail title) `sceAgcDriverSubmitAcb` returned 0 and ran a label-only buffer on queues 0x20, 0x30, 0x40, 0x50, 0x57 and also 0x58, which throws here; queue 0x1F faulted inside the console's driver (SIGSEGV in libkernel on the calling thread), which throws here too. Queues above 0x58 and `sceAgcDriverSubmitMultiAcbs` were not measured. Closes when: the console's highest accepted compute queue is measured +- [sceAgcDriverSubmitAcb, sceAgcDriverSubmitMultiAcbs](../../../core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp) (libSceAgcDriver) accept compute queues 0x20 to 0x57 and throw for any other. On a PS5 (firmware 13.40, measured 2026-10-19 from inside a retail title) `sceAgcDriverSubmitAcb` returned 0 and ran a label-only buffer on queues 0x20, 0x30, 0x40, 0x50, 0x57 and also 0x58, which throws here; queue 0x1F faulted inside the console's driver (SIGSEGV in libkernel on the calling thread), which throws here too. Queues above 0x58 and `sceAgcDriverSubmitMultiAcbs` were not measured. Closes when: the console's highest accepted compute queue is measured. 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. -- [sceAgcDriverAcquireHDRScopesACQ, sceAgcDriverHp3dLockEnd, sceAgcDriverHp3dLockStart, sceAgcDriverIsEsvmProcess, sceAgcDriverNopDataWritePacket, sceAgcDriverReleaseHDRScopesACQ, sceAgcDriverSetValueToSharedGds, sceAgcDriverSetWorkloadStreamInactive, sceAgcDriverSubmitReprojectionAcb, sceAgcDriverSuspendPointSubmitCdbg, sceAgcDriverSysGetGfxAppGpuResetStatus](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - signatures and behavior are unverified; these parameterless declarations only resolve imports and throw through `NotImplemented_nid_no_patch`. +- [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`. +- [sceAgcDriverNopDataWritePacket](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - writes an IT_NOP packet with 32-bit address at dword 1 and payload at dword 2, returning 2 dwords for null data or zero bytes, and total dwords otherwise. Console behavior for null or unaligned destination remains unmeasured. - [sceAgcDriverGetCoredumpAddress](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) returns the driver's shared coredump-area pointer, which is null when no coredump area is allocated. AnyPS5 reports coredump mode disabled and does not allocate this area; the export returns null. Enabling and populating an AGC coredump area is not implemented. - [sceAgcDriverSetTargetRingForDiag](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) returns `0x8A6D0001` (`DRIVER_FEATURE_UNAVAILABLE`), matching the retail PS5 export. The retail diagnostic log message is not reproduced. @@ -33,6 +34,7 @@ - [sceAgcDriverSetSubmitValidationMode, sceAgcDriverGetSubmitValidationMode, sceAgcDriverSetSubmitValidationConfig, sceAgcDriverGetSubmitValidationConfig, sceAgcDriverSetValidationErrorOutputFrequency, sceAgcDriverGetShaderDebuggingStatus](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - the five validation calls return `0x8A6C1000` without reading their arguments or writing caller memory, and `sceAgcDriverGetShaderDebuggingStatus` returns 1, as [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L250-L291) reads them from Sony's libSceAgcDriver.sprx: each validation export is `mov eax, 0x8a6c1000; ret` in the copy Dragon Quest VII Reimagined bundles (0x7bb0-0x7bf0), and the debugging status is 1 in the firmware 5.50 library (0x6740). The parameters (a 32-bit mode or frequency, a mode output, a configuration pointer) follow OpenAGC and are never read. Whether later retail firmware enables submit validation or shader debugging is unverified; no title in this repository is known to call them - [sceAgcDriver retail constant results](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - the debug, capture, GPU printf, register access, GPU reset, suspend point and user data exports taken from the retail `libSceAgcDriver.sprx`, and `sceAgcDriverRegisterOwnerForSystem`, `sceAgcDriverInitResourceRegistrationInterface` and `sceAgcDriverSetResourceRegistrationInfo` in [Registration.cpp](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp), are declared without parameters: the console bodies return a constant without reading any argument, so their signatures are unknown. `sceAgcDriverAllocateToolMemoryForGpuReset` and `sceAgcDriverGetAllocatedToolMemoryForGpuReset` read only the output pointer (fourth and second argument, a 12-byte range the console zeroes); the other arguments are declared as `uint64_t`. - [Indices far past every vertex fetch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - when a draw's highest index is at least 64Ki vertices past the largest per-vertex record count, every index past that count is moved to it in a copy of the indices, so the zero-padded vertex copies are not sized to the original index (PPSA04159 draws a list of `0xffffffff` with primitive restart off, which asked for 160 GiB). Each attribute fetched by vertex index reads zeros there as at the original index (out-of-bounds select 0 or 1); a draw with another out-of-bounds select keeps its indices. The vertex index the shader sees changes for the moved indices, which a shader that reads it directly would notice +- [Depth bounds without the depthBounds feature](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - on a device without `depthBounds` (MoltenVK), a depth bounds draw with no depth test or write, a 32-bit float depth target, a pixel program that neither reads nor exports depth, and no stencil write on a failed test is drawn with a read-only depth test against one bound pinned as the viewport depth range: `GREATER_OR_EQUAL` against the upper bound when the lower one is 0, `LESS_OR_EQUAL` against the lower bound when the upper one is 1. This takes stored depth to stay in [0, 1], as it does without `VK_EXT_depth_range_unrestricted`. Both bounds inside (0, 1), depth testing, normalized depth targets and the other cases still need the feature and throw. - [Depth clear draws](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a draw with `DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE` is taken to write `DB_DEPTH_CLEAR` to every pixel it covers whatever the depth test, depth bounds and depth bias say: it is drawn with an always-passing depth write and the viewport depth range pinned to the clear value. The draw is rasterized as submitted, so its coverage is the draw's own. Whether the DB still applies the depth test or HiZ to such a draw is not measured - [GS_FAST_LAUNCH geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_SHADER_STAGES_EN.GS_FAST_LAUNCH = 1` with a point list and `GE_CNTL` groups of one vertex and one primitive decodes as one point per subgroup, 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`, the workgroup index comes from the vertex id of the first thread), not measured on the console. Fast launch 2, other group sizes or input primitives 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. @@ -51,6 +53,7 @@ - [Strided buffer overhang](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a strided buffer descriptor that ends less than one stride past the end of the readable registered allocation holding its base is bound only up to that allocation's end, even when another readable allocation starts right there. This assumes the bytes past the end are the rounded-up `num_records` of the last record (RADV rounds it up), which no fetch inside the buffer reads; a guest that really keeps a vertex split across two adjacent allocations would read zeros for the cut tail. The same holds for a read-only strided buffer that starts below the render target and runs less than one stride into it: it is bound only up to the target, as a vertex fetch whose attribute descriptor starts at the attribute's offset reaches that far past the attribute's last bytes. Taken from the RetroArch Dolphin core case in #2912, not measured on hardware. - [Buffer descriptor CACHE_SWIZZLE hint](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - bit 30 of word 1 in a buffer resource descriptor (`words[1] & 0x40000000u`) is normalized to zero when binding guest buffers in `ShaderResources::addGuestBuffer`. On GFX10 (gfx1013), bit 30 of V# word 1 is `CACHE_SWIZZLE` (Mesa's `gfx10-rsrc.json`: STRIDE [29:16], CACHE_SWIZZLE [30], SWIZZLE_ENABLE [31]), a cache-bank hint with no effect on the address or data, which is dropped here. - [Buffer descriptors past the range limit](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a buffer V# whose size is over `maxStorageBufferRange` is bound only up to the end of the readable registered allocation holding its base when that allocation ends before the limit, and as the empty placeholder buffer, like a null V#, when no registered allocation holds its base; a V# over an allocation that reaches the limit still throws. Titles leave such words in descriptor slots their shaders do not reach on the path taken (PPSA10528 binds `c8010002 2c6bec20 c0053c00 04000013`, about 33 TB from an address above the user address space). What the console returns for an access past the bound range is not known; here the device's robust buffer access decides. Follows the review of #3202, not measured on hardware. +- [Guest buffer region over 4 GiB copied whole](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - `GuestBufferMemory::copyRegion` copies a guest buffer region over 4 GiB whole into one HOST_VISIBLE|HOST_COHERENT buffer of the region's size (0x102000000, 0x142000000 and 0x102000000 bytes in three runs of PPSA15246 on an AMD RX 6400), and `vkAllocateMemory` refuses it with -13 although the host heap (31961 MiB) had at most 5933 MiB in use; only a sparse region is cut to its backed and viewed extent. An RTX 4080 SUPER stops at the descriptor range check in `GuestBufferMemory::Descriptor` for the same shader instead; its `maxStorageBufferRange` was not measured. Which part of the region the shader reads is unknown. - [Vertex fetch overhang](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - a zero-padded vertex fetch whose valid range (`num_records * stride`) ends less than one stride past the end of the readable memory at its base copies only the readable bytes and reads the rest as zeros, instead of failing the draw; a longer overhang still fails. This extends the strided buffer overhang rule above (the last record is the rounded-up `num_records`) to the staging copy and is not measured on hardware. The observed case is a fetch of 14858 records of 44 bytes that ends 8 bytes past the end of the 0xa0000-byte mapping holding it. - [Zero-stride vertex fetch range](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a draw whose highest index is at or past a nonzero NUM_RECORDS of a stride-0 `OOB_SELECT` 1 V# throws: the pipeline binding has stride 0, so vertices cannot differ. Only the highest index is checked, so a draw with every index past it throws too, where the hardware reads zero. An indirect draw is not checked. Unmeasured on gfx1013 (the console), with an instruction offset or SOFFSET, with a swizzled V# and in a vertex shader. Rules from #8299, measured on an RX 6700 XT (gfx1031), same results on gfx1010, with raw ISA ([data](https://gist.github.com/thuanquang/3b25d677c1a32d79123dd2df1819dd83)). Stride 0 reads zero for `OOB_SELECT` 0 always; for 1 when index >= NUM_RECORDS (records); for 2 when NUM_RECORDS is 0; for 3 when NUM_RECORDS is under the fetch's bytes. Untyped loads check each dword under 3; only typed fetches are used here. - [Resource registration](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws @@ -70,31 +73,33 @@ ### Functional - [Written staging window on discrete GPUs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - a written buffer element of a dispatch that is not swept is staged device-local up to 8 MiB on a discrete GPU (`Context::discreteGpu`) and up to 2 MiB otherwise; `APS5_WRITTEN_SHADOW_MAX_KIB` replaces either bound. The 8 MiB figure comes from one title on one NVIDIA Windows system (PPSA09911: a 2.5 and a 3.3 MiB skin-cache output written in place cost 55 ms of GPU time per frame, 1.8 ms of extra staging copies once staged); the point where the two copies of a larger element cost more than its in-place stores, and whether AMD or Linux drivers move it, has not been measured. The window is per element and per use, so a kernel that streams once through an element inside the window pays the copy-in and the copy-back on every dispatch. -- [Frame capture](../../../core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp) (libSceAgcDriver) records guest memory on Windows only (a VirtualQuery walk of the registry allocations and the Windows write tracker); elsewhere a capture throws when it starts. Ranges are captured by allocation and protection, not by mapping: section views, aliases of one direct memory block and file views are snapshotted and replayed as separate private memory, so a store through one alias is not seen through another, and CPU stores to them during the captured frames are not recorded (only write-watched private memory is). Pages of the title's image are recorded during the frames only when a shader header or an array it points to lies in them and their content changed. A capture starts only at a frame boundary where the driver goes idle within 10 s, and gives up after five boundaries that do not. +- [Frame capture](../../../core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp) (libSceAgcDriver) records guest memory on Windows only (a VirtualQuery walk of the registry allocations and the Windows write tracker); elsewhere a capture throws when it starts. Ranges are captured by allocation and protection, not by mapping: section views, aliases of one direct memory block and file views are snapshotted and replayed as separate private memory, so a store through one alias is not seen through another, and CPU stores to them during the captured frames are not recorded (only write-watched private memory is). Pages of the title's image are recorded during the frames only when a shader header or an array it points to lies in them and their content changed. A capture starts only at a frame boundary where the driver goes idle within 10 s, and gives up after five boundaries that do not. A capture also stops when GPU work in flight at a delta waits on a later CPU store. - [Frame replay](../../../core/libs/prx/libSceAgcDriver/tools/FrameReplay.cpp) (libSceAgcDriver) orders the calls of different captured threads only by their start (a call starts once every earlier call has started), so the driver sees a schedule the title could have produced, not necessarily the one it did. CPU stores a title made from threads that never call the driver are known only by the next recorded call, so the replay applies them early or late when a GPU wait needs them and derives a value from the wait's reference when no recorded store satisfies it (`of_them_synthesized`). Pacing compares the number of submissions in flight, not what they hold. Prologue registrations of shaders whose memory the title reused after registering them fail and are counted (`stale_registrations`). The title's image range must be free in the replaying process, so the tool is linked at a high image base on Windows. - [Workgroup count split](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) - a direct dispatch over `maxComputeWorkGroupCount` runs as `vkCmdDispatch` chunks, each bound to a pipeline variant whose workgroup offset specialization constants add the chunk's base to `WorkgroupId` and `GlobalInvocationId`. A variant is built when its chunk base is first recorded, not when the dispatch is prepared. A split of a shader with device-memory LDS throws: its slot base reads the chunk's `NumWorkgroups`, not the dispatch's. Chunks are ordered by a compute barrier, which the console does not have between workgroups of one dispatch. - [Experimental submission progress](../../../core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionProgress.hpp) (AGC driver) - the opt-in two-millisecond pacing budget is not derived from console hardware. Cross-title performance remains unvalidated; host scheduling can overshoot the timeout and pacing can reduce CPU/GPU overlap. - [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. - [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. - [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) import uniform 0000/0001/1110/1111 keys into a single-layer, level-zero SW_64KB_R_X color image with 2, 4 or 8 fragments. Color and DCC/CMASK ranges must stay write-watched and unchanged; expanded or mixed initial DCC, register CMASK clears, compressed payloads, color transforms, lossy modes, other metadata changes, guest publication and writes over a live owner throw. A whole-range DCC fill with one of those keys, landed or still recorded, is taken as its fast clear; a partial fill, or a CMASK changed without a whole fill, throws. Driver-mediated reads of the color and metadata stay guarded. The metadata extent follows [AMD Addrlib](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L347-L454) with the [PS5 Mesa model](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/winsys/ps5/radv_ps5_winsys.c): GFX10.1, 16 pipes, 256-byte interleave, two maximum compressed fragments. Console geometry, concurrent guest mutation and transitions outside this subset are unverified. +- [Native multisampled DCC fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) take only uniform 0000/0001/1110/1111 keys, 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. 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. - [Initial stencil state](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) uses zero for `DB_STENCIL_CONTROL`, `DB_STENCILREFMASK` and `DB_STENCILREFMASK_BF` (context `0x10b`-`0x10d`), including after `CLEAR_STATE`. The public V0, V4, V5 and V7-V11 [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) supply these values. Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h) agrees on the control register but uses `0x01000000` for the reference masks; the console reset values have not been measured. Planet Coaster (PPSA01736 v01.017) enables stencil tests without writing the control register. - [Multisampled DCC comp-to-single clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (AGC driver) - RADV's multisampled comp-to-single clear dispatch ([`radv_clear_dcc_comp_to_single`](https://github.com/mihawk-99/PS5_Mesa/blob/1ba9874f478657ccf6da72ce55f0260164cc0bd0/src/amd/vulkan/meta/radv_meta_clear.c#L954)) is recognized by its code and not run. Its clear color (user data 5 to 8, which the shader stores as the four dwords of sample 0's texel) is recorded for the surface its image descriptor names, and DCC keys of 0x10 on a multisampled target of that surface, pipe-bank XOR, size and sample count import as a clear to it, for texels of 8 to 128 bits. A record is dropped when an unmap or allocation removal covers its surface's base. The guest's color memory doesn't get the color at the blocks' first pixels, and the recorded color is assumed to be the one the keys refer to: RADV writes 0x10 keys only in a fast clear that runs this dispatch for the same image right after, and initializes DCC to 0x00 or 0xff on an image's first use ([`radv_init_color_image_metadata`](https://github.com/mihawk-99/PS5_Mesa/blob/1ba9874f478657ccf6da72ce55f0260164cc0bd0/src/amd/vulkan/radv_cmd_buffer.c#L16894)). A new image on memory reused within one mapping, with the same address, XOR, size and sample count, whose clear runs a comp-to-single shader other than the recognized one, would still import the old color. - [Initial depth and stencil planes](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) - the console's reset values of `DB_Z_INFO` and `DB_STENCIL_INFO` (context `0x010`-`0x011`) are not measured. Both start, and return after `CLEAR_STATE`, at zero (formats `INVALID`), the value of Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h). The public [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) write `0x80000180` and `0x20000180` instead, also with formats `INVALID`; of their other bits, only `TILE_STENCIL_DISABLE` would change a result: the HTILE layout of a multisampled depth plane whose `DB_STENCIL_INFO` was never written. -- [Type-0 and type-1 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 or 1 in dword 3) with a nonzero base are bound as null images, like a zero base. TYPE 0 comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault). TYPE 1 comes from a gfx1010 (RDNA1) measurement in #4139 and #5828 (`image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` return zero, no fault) with format 22 only; TYPE 1 is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). Neither type was measured on the console's gfx1013. -- [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. The next target, sampled or storage lookup removes retired cache owners; recorded work and sampled views keep their images alive until their last reference is released. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. +- [Type-0 and type-1 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 or 1 in dword 3) with a nonzero base are bound as null images, like a zero base. TYPE 0 comes from a gfx1036 (RDNA2) measurement in issue #4139, TYPE 1 from a gfx1010 (RDNA1) measurement in #4139 and #5828 with format 22 only. On a gfx1100 (RDNA3, RADV Mesa 25.2.8) both types answer zero with no fault from `image_sample_lz`, `image_sample_b`, `image_load`, `image_sample_c_lz` and `image_get_resinfo`, for formats 32_FLOAT, 16_SSCALED, 2_10_10_10_SNORM and 8_UNORM (GFX11 codes 22, 10, 37, 1), over a readable nonzero base and a zero base ([hardware oracle](../HW_ORACLE.md), 2D T# declared 64x32). The TYPE 9 control answers the stored texel, 64 and 32 from resinfo, and 1.0 from an ALWAYS `image_sample_c_lz` at 32_FLOAT, the only one of these formats gfx1100 compares. TYPE 1 is not measured on RDNA2, and neither type on the console's gfx1013. +- [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. The next target, sampled or storage lookup removes retired cache owners; recorded work and sampled views keep their images alive until their last reference is released. A target made from a multisampled texture that a sample reads before any draw is retired instead when a guest access overlaps it, until a draw, a dispatch's storage write or an absorbed color fill puts samples on the host only; it is rebuilt from guest memory at its next use. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. - [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` replays the low eight bits of the values proved from constant, select and phi exports, with an export proved on every kept path. An export the prover cannot bound replays all 256 eight-bit values, one pass per value per triangle (logged once per process). It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions (a ballot of the active lanes, as in an EXEC copy, covers whole quads and is accepted), since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults, and read-only BDA reads may report faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. - [32-bit depth-bits reads of guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): a sampled 32_UINT or 32_SINT T# with a depth swizzle mode that no live depth surface matches reads the guest bytes through an R32_SFLOAT image, as the recompiler's depth-bits read expects, and the shader casts the fetched value back to the stored dword. On RX 6700 XT (gfx1031, AMD Windows driver 32.0.21045.5002) the read keeps float denormals, the sign bit alone and quiet and signaling NaN payloads bit for bit. A host that canonicalizes NaNs or flushes denormals on a 32-bit float fetch would change those dwords, and other drivers are not measured. A live depth surface whose extent or base level differs from the T# is not used; the read takes the guest bytes, as the 32-bit float and 16-bit views already do. - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A dispatch of the 15-word 8x8x1 image clear kernel ([`MatchStorageImageClear`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp): one colour read from the V# in `user[8..11]` stored to every texel of the T# in `user[0..7]`) that covers the whole plane and stores 0 or 1 through a 16-bit unorm view, 0 or 0xffff through a 16-bit uint view, or 0 or a normal float in (0, 1] through a 32-bit float or integer view leaves out the copy-in and replaces the copy-out by a clear of the depth image to that value; the dispatch still writes the storage image. The colour is read again when the copy-out is replaced, and a changed value keeps the copy-out. Indirect dispatches, partial last groups, dispatches that queue on the draw pipeline, other values and formats, a plane the kernel covers only partly and stencil planes keep both copies. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube or a 2D array of more than one layer of the stencil plane or from a later slice, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. - [Collapsed color array views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) preserve the selected slice in the XOR address equation when the view is represented as a shifted 2D surface. Native Vulkan coverage checks R_X draws, array reads, publication and re-upload for slices 1-3; Lost Soul Aside PPSA26358 validates the RGBA16F cube mip chain on Windows/NVIDIA. Other devices and console comparisons remain unverified. Full-array views retain their layer addressing. -- A [depth surface](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is retired when a color target overlaps it, an unserved storage view writes a whole plane, or it is sampled through a view it cannot serve and guest memory over it was written since it was last bound; lookups then skip it, so sampling reads the memory like any texture. Bound again, it keeps its own depth instead of the bytes written over it, except for the depth planes the next entry keeps in guest memory. Without write watching, a view it cannot serve still throws. A retired surface also gives up its depth and stencil planes, so another surface sharing them no longer copies from it. +- A [depth surface](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is retired when a color target or a written storage image it does not serve overlaps it, or when it is sampled through a view it cannot serve and guest memory over it was written since it was last bound; lookups then skip it, so sampling reads the memory like any texture. Bound again, it keeps its own depth instead of the bytes written over it, except for the depth planes the next entry keeps in guest memory. Without write watching, a view it cannot serve still throws. A retired surface also gives up its depth and stencil planes, so another surface sharing them no longer copies from it. - [Depth planes in guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): only single-sample `D32_SFLOAT` and `D32_SFLOAT_S8_UINT` planes with the 64 KiB Z swizzle (`SW_MODE` 24) reach guest memory; D16, other swizzles, stencil and multisampled planes keep depth in the host image only. A plane drawn since its last store is stored, uncompressed, in 64 KiB units (128x128 tiles): a buffer upload, a `GuestBuffers` V#, an image lookup or a pending-store flush stores the units it overlaps, a color target or written storage image over the plane all of them. CPU reads and reads through address-based heaps are not caught. Guest writes are ordered when collected (bind, flush, collect epoch), not when made: a collected write reloads the units it touched unless an HTILE clear is pending. A store outside a recorder waits for the GPU. The console keeps HTILE-compressed tiles compressed in memory until a decompress (Mesa `ac_descriptors.h`). - [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 or a cube, 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 with other than 1 or 6 slices, 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). +- [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. - [SW_64KB_D_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the existing texture block/mip geometry and equations from [AMD addrlib GFX10 non-RB+](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) for 16 pipes and 256-byte interleave. The address-reference tests use legal pipe/bank XOR bits for that configuration. Raw column bits outside the source configuration's pipe/bank mask 0xcf00 throw; their console meaning is unresolved. Console address equations and render results have not been measured. @@ -110,25 +115,30 @@ - [Unused pixel programs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): a bound pixel program is skipped when color masks have no intersection and DB export and execution enables are clear, regardless of retained SPI_SHADER_Z_FORMAT. This follows [AMD PAL DbShaderControl](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9AbiToPipelineRegisters.h) and its [GFX10 register definitions](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/chip/gfx9_plus_merged_registers.h), not a hardware measurement. Enabled DB_ALPHA_TO_MASK conservatively retains the program even with ALPHA_TO_MASK_DISABLE, because DecodeState currently enables Vulkan alpha-to-coverage without applying that override. Console behavior for these combinations has not been measured. - [Sample masks](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `PA_SC_AA_MASK_X0Y0_X1Y0` and `PA_SC_AA_MASK_X0Y1_X1Y1` hold one 16-bit sample mask for each pixel of the aligned 2x2 quad ([Mesa gfx10.json](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/registers/gfx10.json#L13512)); only the bits of the `MSAA_NUM_SAMPLES` rasterized samples count, and at one sample bit 0 of a pixel's field covers every sample of the target the draw is raised to. A mask equal for the four pixels is the Vulkan `pSampleMask`, the case [radv](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_cmd_buffer.c#L13633) and [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/gallium/drivers/radeonsi/si_state.c#L4218) program; with early fragment tests it needs `earlyFragmentSampleMaskTestBeforeSampleCounting` ([radv](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_physical_device.c#L2079) reports it). A mask that differs between the pixels is applied in the pixel shader without early fragment tests: a pixel whose mask covers all or none of its samples is demoted to a helper invocation by `gl_FragCoord` parity and killed when the program returns (the Radeon 860M's Windows driver 32.0.13058.8001 writes undefined colour for a demoted pixel whose export leaves its output unwritten), and otherwise the pixel's mask is ANDed into `gl_SampleMask` (with alpha-to-coverage and the mask export) after demoting invocations whose `SampleMask` input it leaves empty. A pixel program with early Z that writes memory under depth, stencil or depth-bounds tests is therefore refused. That XnYm is the pixel at (x mod 2, y mod 2) = (n, m) is read from the field names; that the mask removes samples before the depth and stencil tests and occlusion counting is inferred from radv implementing the Vulkan sample mask test with it; and that it takes no part in the centroid (`AA_MASK_CENTROID_DTMN` 0) is inferred from radv setting that bit only for conservative rasterization. None of it is measured on gfx1013. A mask equal for the four pixels that removes some but not all samples of a pixel is refused with active centroid inputs, since where Vulkan puts the centroid relative to `pSampleMask` is implementation-defined, and any partial mask is refused with conservative rasterization (radv sets `PREZ_AA_MASK_ENABLE` and `POSTZ_AA_MASK_ENABLE` [there](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_cmd_buffer.c#L13653), which 0x6001 leaves clear). - [Covered-centroid raster state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) accepts PA_SC_AA_CONFIG.COVERED_CENTROID_IS_CENTER only when the pixel program is skipped or SPI_PS_INPUT_ENA and SPI_PS_INPUT_ADDR have no active perspective or linear centroid input. Bit 29 follows [PS5_Mesa GFX10.3 registers](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/registers/gfx103.json); [RADV](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/radv_cmd_buffer.c) sets it on GFX10.3 and later. Active centroid interpolation and unspecified input registers still throw. Console covered-pixel centroid results were not measured. +- [Buffer destruction on MoltenVK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DeferredDestruction.cpp) (libSceAgcDriver): on MoltenVK, buffers and device memory released through the graphics context are destroyed only while the graphics queue is idle. MoltenVK makes every live `SHADER_DEVICE_ADDRESS` buffer resident, unretained, in each command buffer that uses device addresses, also in ones submitted after the release, and passes Metal a nil buffer once its memory is freed ([KhronosGroup/MoltenVK#2876](https://github.com/KhronosGroup/MoltenVK/issues/2876)). A recorder or command batch submission drains the queue under the GPU mutex once 512 objects wait or the oldest has waited 100 ms (`vkQueueWaitIdle`, then the buffers, then the memory), which stalls the CPU until the GPU is idle; memory released under pressure only returns at that drain. `VulkanDevice`'s own destroys are not deferred. `APS5_NO_DEFERRED_DESTRUCTION=1` turns this off; remove it once MoltenVK fixes #2876. - [Compute LDS beyond the device's shared-memory limit](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords, rounded up to an even count so that 64-bit atomics stay 8-byte aligned, per workgroup (the dword after the LDS is the atomic lock on a device without `shaderBufferInt64Atomics`), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. - [AGC buffer allocations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request, except for the device buffers and images that the "Host placement after a device-memory refusal" entry places in host memory. - [Storage-cache pressure and idle device buffers](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp): the one-second idle observation interval is a retention choice, not a console rule; buffers reused less often can be recreated under pressure. Before a storage-cache eviction, independently allocated device-tier buffers unused throughout that interval can be released, oldest first, for the cache's shortfall below its existing ceiling. Live buffers, host tiers and slab allocations are not reclaimed this way. Reclamation requires exactly one device-local heap because retained buffers do not record their heap. The heap budget is read again after an actual release; a delayed driver report can still cause an eviction. This is not a shared budget for all retained GPU resources, and its effect on titles with intermittent large buffer use is unmeasured. - [Host placement after a device-memory refusal](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) (libSceAgcDriver): when the driver refuses a device-local allocation with `VK_ERROR_OUT_OF_DEVICE_MEMORY` after the pool's trim and the texture-cache release, device buffers and images (textures, storage textures, block, repacked, depth, depth plane and stencil copies, render, depth and multisampled targets and attachment proxies) are allocated in a host memory type their requirements allow. A host-placed device buffer leaves the pool on release. A host-placed image keeps that placement until it is destroyed and is never moved back to device memory, so every access to it crosses PCIe for its lifetime. The placements are counted in a rate-limited `[bufferpool]` line with the cumulative MiB, not the resident bytes. The 1x1 denormal clear probe still allocates device memory directly and throws on a refusal. -- [Pipeline journal](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp) (libSceAgcDriver): the journal is append-only until it reaches `APS5_PIPELINE_JOURNAL_MAX_MIB`, when it is rewritten to three quarters of the cap with the newest pipelines and their modules; the rewrite streams kept records; a replay thread reads an entry's modules when it builds that entry. Only the last `APS5_PIPELINE_JOURNAL_ENTRIES` pipelines are replayed, and the store evicts the least recently used entry past `APS5_PIPELINE_STORE_ENTRIES`: a title with more configurations rebuilds the rest at the draw. A compute pipeline is replayed over the set layout `DescribeSetLayouts` derives from the result's bindings, checked against its `ShaderResources` layout only under `APS5_VERIFY_SET_LAYOUTS=1`. Recording and replay are best effort: a record whose write fails is lost and an entry whose build fails is skipped, both counted in the `[pipeline-journal]` line. The file header's format number guards the recipe codec and the store key. +- [Pipeline journal](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp) (libSceAgcDriver): resident graphics pipelines are bounded by count, not bytes: the store grows from 1024 to 4096 entries for a replay that does not fit and for evicted pipelines that keep coming back, and the last `APS5_PIPELINE_JOURNAL_ENTRIES` (4096) records are all replayed, so a pipeline the title never asks for again costs its memory until teardown. `agc_driver_recorder_tests --benchmark-pipeline-store-memory [count]` gives a floor with the triangle shaders: about 59 KiB host and no device memory per pipeline on an RTX 4090, 100 KiB host and 8 KiB device-local on a UHD 770; larger shaders cost more. The file is rewritten to three quarters of `APS5_PIPELINE_JOURNAL_MAX_MIB` when full. A compute pipeline is replayed over the layout `DescribeSetLayouts` derives, checked only under `APS5_VERIFY_SET_LAYOUTS=1`. The header's format number guards the recipe codec and the store key. - [Graphics pipeline libraries](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline - [Fragment shader interlock pipelines](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver): a pipeline whose fragment shader declares a fragment shader interlock capability is never built from graphics pipeline libraries, on every driver, so it is compiled whole at the draw with no fast link to bridge the compile. RADV's fast link ([mesa#16653](https://gitlab.freedesktop.org/mesa/mesa/-/work_items/16653), Mesa 26.2.5, RX 7900 GRE) jumps to the PS epilog without waiting for the shader's stores, and the epilog's `done` export ends the GFX11 primitive-ordered section, so the next overlapping wave reads the old value: `agc_driver_pixel_interlock` lost read-modify-writes in 108 of 120 loaded runs under `APS5_NO_PIPELINE_LTO=1`. On an RTX 4090 (NVIDIA 617.42, Windows) the fast link kept the ordering in 240 of 240 loaded runs; other drivers are not measured. - [Prebuilt pipeline libraries](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver): each registered vertex or pixel shader in registration form gets its pre-rasterization or fragment-shader library built on one background thread, with a fixed state the draw may not use: triangle list, counter-clockwise front face, no depth clamp, depth bias or conservative rasterization, first provoking vertex, one sample. Libraries are kept until the device is torn down; on Astro's Playroom 1948 of them added 0.44 GB of peak memory on RADV. - [`APS5_THREAD_PRIORITY=rt`](../../../core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp) (libSceAgcDriver) lowers the process's hard `RLIMIT_RTTIME` when it is above `APS5_THREAD_RTTIME_US` or rtkit's `RTTimeUSecMax`, because [rtkit](https://github.com/heftig/rtkit/blob/c295fa849f52b487be6433e69e08b46251950399/rtkit-daemon.c#L592-L637) (`verify_process_rttime`) reads the hard limit from `/proc//limits` and refuses real-time scheduling when it is higher. An unprivileged process cannot raise the hard limit again, so it stays lowered for the process and its threads even when rtkit then refuses the request (burst limit, user or start-time checks) and the thread throws. rtkit's `MaxRealtimePriority` and `RTTimeUSecMax` are read first, so an unreachable rtkit or a priority above its maximum leaves the limit untouched. The lowered limit only matters for threads that later run under a real-time policy - On a device without VK_EXT_robustness2's `nullDescriptor`, such as MoltenVK, a null image descriptor in a typed heap is a [placeholder image](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp) of that heap's type, 1×1 and cleared to zero, which every null descriptor of the heap shares. Reads give zeros, as `nullDescriptor` allows, but stores and atomics to it are kept, so a later read of a null storage image can return them, and a depth comparison compares against depth 0. A heap whose placeholder the device cannot create, for example 1D or 3D depth, 64-bit atomics without 64-bit image atomics, or multisampled storage without `shaderStorageImageMultisample`, throws when a null descriptor reaches it. - [Draw input snapshots](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. -- [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. Explicit-size file writes (`write`, `pwrite`, `writev`, `pwritev`, AIO and `fwrite`) synchronize their source ranges through the [host-read hook](../../../core/libs/prx/libc/src/GuestArena.cpp); other host readers are not covered. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. +- [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver), except drawn resident linear targets (next entry), are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. Explicit-size file writes (`write`, `pwrite`, `writev`, `pwritev`, AIO and `fwrite`) synchronize their source ranges through the [host-read hook](../../../core/libs/prx/libc/src/GuestArena.cpp); other host readers are not covered. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. +- [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. - [`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), 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. A range that leaves the watch without the hook running over it (an import fallback that unwatched without saving) leaves its pending results without saved bytes; storing such results throws. A unit whose results were stored while its blocks were watched keeps only a generation; when its blocks leave the watch while nothing is pending, the hook saves nothing, and results that land in it afterwards have neither saved bytes nor a watch, so storing them throws too. +- [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. A range that leaves the watch without the hook running over it leaves its pending results without saved bytes; storing such results throws. A unit whose results were stored while its blocks were watched keeps only a generation; when its blocks leave the watch while nothing is pending, the hook saves nothing, and results that land in it afterwards have neither saved bytes nor a watch, so storing them throws too. +- [Storage images over uncommitted guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) - a storage image whose guest range has no committed page when it is created throws in the `StorageTexture` constructor, which aborts the guest. Several titles reach this intermittently in runs of a few minutes, on AMD and NVIDIA alike; in two of the three logs, lines about storage images over released or rewritten memory (`memory no longer registered`, `guest memory changed while GPU results were pending`) come shortly before it. The log does not name the image's address. What the console does with a storage descriptor over memory that is not committed is unknown, and which allocation the guest recreates there was not found. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed, unless a storage image overlaps them (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. -- [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) whose protection an import resolves count as stored only when their bytes change. A same-value CPU store during that window cannot be distinguished from driver activity and does not win over pending results. On Windows, NVIDIA 617.42 marks every private MEM_WRITE_WATCH page after import until a GPU use completes. The driver behavior was measured on this driver; other drivers are unverified. Imports of at least 1 MiB wait for a four-byte read on the separate settle queue inside the window. Smaller imports stay compared until a deferred read completes; a later lookup returns whole blocks to write watching with a new CPU-store stamp. Blocks overlapping a live storage image, blocks of an import retired before rewatching, and imports without a settle queue stay compared. Shared-memory imports retain their separate-alias tracking. -- [Deferred host-import reads](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) use the main queue with barriers ordering earlier and later writes around the reads. Submission does not wait on the CPU. The latency cost of placing batches behind main-queue work is unmeasured. The first-use test now checks that earlier main-queue reads finish before the import returns to write watching; the original two-queue canary measured driver marking while those reads were still in flight. The earlier-import canary still checks later use of another import of the same pages. +- [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) whose protection an import resolves count as stored only when their bytes change. A same-value CPU store during that window cannot be distinguished from driver activity and does not win over pending results. On Windows, NVIDIA 617.42 marks every private MEM_WRITE_WATCH page after import until a GPU use completes. The driver behavior was measured on this driver; other drivers are unverified. Imports of at least 1 MiB wait for a four-byte read on the separate settle queue inside the window. Smaller imports stay compared until a deferred read completes; a later lookup returns their pages to write watching, stamping their blocks as CPU stores; the rest of a partly covered block stays compared. Blocks overlapping a live storage image, blocks of an import retired before rewatching, and imports without a settle queue stay compared. Shared-memory imports retain their separate-alias tracking. +- [Deferred host-import reads](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) use the main queue with barriers ordering earlier and later writes around the reads. Submission does not wait on the CPU. An initial multisampled DCC import needing a range whose import still awaits its read submits it if needed and waits under the GPU mutex (`WatchedAfterSettle`, #9724). Imports of partial 64 KiB blocks get no deferred read; only that path reads them, returning their pages as CPU stores unless a live, trimmed or parked compared import or storage image shares their blocks; one a recorder holds is unseen. The latency cost of placing batches behind main-queue work is unmeasured. The first-use test now checks that earlier main-queue reads finish before the import returns to write watching; the original two-queue canary measured driver marking while those reads were still in flight. The earlier-import canary still checks later use of another import of the same pages. - [Settled dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a dma-buf import of 1 MiB or more is read by a 4-byte copy on the settle queue right after it is made (`settleImport`), so the kernel maps its pages for the GPU there and not in the first submission that uses it (about 2 s for a 3 GiB import on RADV). The import's buffer is shared with the settle queue's family. An import made at map time settles on the guest thread that maps the range, inside its map call, so that thread waits while the kernel maps the pages: 0.47 to 2.2 s for the 3 GiB range of Hades (PPSA03355) and 0.49 to 4.3 s for the 4.5 GiB range of ASTRO BOT (PPSA21567) on RADV. Both titles map these ranges at start-up; a title that maps large ranges during gameplay stalls its mapping thread for as long. An import made from a submission settles under the GPU mutex, so its frame still pays for it; a smaller import, and every import on a device with no settle queue (one without a copy-capable queue besides the main one), pays in its first submission. A range imported through a host pointer, as lavapipe imports shared memory, is not settled - [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range whose offset in the backing file is not a multiple of 64 KiB starts its sparse buffer at the 64 KiB block below it (`HostImport::lead`). A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when the buffer's sparse block size is larger than 64 KiB (it is 64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory +- [Presentation under the GPU mutex](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) - on a device whose presentation family has no queue left after the main and settle queues, such as RADV, lavapipe and Intel's Windows driver (Arc Graphics on Meteor Lake, 101.9150), which expose a single graphics queue, `vkQueuePresentKHR` still runs on the main queue under `GuestMemory::GpuMutex`, so the draw committer and the queue workers wait for as long as the present blocks. How long that is on those drivers was not measured; on NVIDIA in FIFO mode it is about 6 ms per present. A device with a free queue in that family presents on it under its own mutex, after the GPU mutex is released. Closes when: a trace on a single-queue device shows no `GpuMutex.wait.*` behind `Vulkan.QueuePresent`. - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. - An [unnormalized S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) maps clamp mode 0 (repeat) on X and Y to clamp-to-edge as required by Vulkan unnormalized coordinates, and assigns the magnification filter when minification and magnification filters differ (since unnormalized coordinates sample at LOD 0). Anisotropy, TRUNC_COORD, MC_COORD_TRUNC and other clamp modes still throw. Secondary LOD bias (LOD_BIAS_SEC, bits [19:14]) is accepted and ignored, matching RDNA2 hardware measurements across explicit and implicit samples. High on Life (PPSA15388, #4736) uses secondary LOD bias and unnormalized coordinates with clamp mode 0 and differing min/mag filters during home base and city rendering. - [TRUNC_COORD](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) is not applied: the texel a point sample reads and the bilinear weights follow the host sampler's rounding. On RDNA 2 (RX 6700 XT), an S# with TRUNC_COORD 0 rounds the exact scaled coordinate to 8 fractional bits, ties to even, and one with TRUNC_COORD 1 truncates it, for point and bilinear filters. AMD's Windows driver (32.0.21045.5002) truncates for nearest samplers and rounds for linear ones. Under it, a TRUNC_COORD 0 point sample at most 1/512 texel below a texel edge reads the texel below the edge where the console reads the one above, and `k / width` computed in f32 lands there for about half of all k. A TRUNC_COORD 1 bilinear weight can be 1/256 above the console's. Other host drivers are not measured. @@ -136,8 +146,8 @@ - [8_SRGB and 8_8_SRGB textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) that the device samples as R8_SRGB or R8G8_SRGB use its own decode, which need not match gfx1035: NVIDIA's R8_SRGB is one bf16 step off in 29 of 256 codes. A device that cannot sample them gets an RGBA8 sRGB copy instead (a blit of the R8 or RG8 texels, refreshed with the storage image's version), whose decode is likewise the device's; 3D textures and storage images that are not single-depth and format-matched are copied from their snapshot instead. - [PM4 submission rings](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp) (libSceAgcDriver) keep only the first 512 dwords of each submission, so the packets past them are not printed. The [failure report](../../../core/libs/prx/libc/src/FailureReport.cpp) dump does not allocate from the C runtime heap: packet names come from static strings, each line is formatted with integer and string conversions into a 512-byte stack buffer and written with one `WriteFile`. It still runs on whatever state the crash left. It waits about 100 yields for each ring's lock and otherwise prints that queue as busy, so a crash while recording a submission loses that queue's ring. It reads the rings without checking them, so a crash that corrupted them can fault inside the dump; such a fault during the report of an unhandled exception stops in that report's `Sleep(INFINITE)` and the process hangs instead of ending. The reports run at most once per process. Linux has no crash report: there the rings are printed only by `APS5_STALL_WATCHDOG_SECS`. - [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) reports a diagnostic after `APS5_GPU_WAIT_TIMEOUT_MS` without treating the timeout as completion. An unsatisfied wait keeps polling and checking driver failures; a missing producer can therefore leave its queue waiting indefinitely. Existing recorded-label shortcuts remain in use. -- A [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) (libSceAgcDriver) on a queue 0x20 label can time out while the awaited word holds the address of another slot of the same label area instead of 0 or 1, with no store to that label in the PM4 ring window (`APS5_PM4_RING`). What writes the address (guest CPU or the emulator) and whether the console would have satisfied the wait before the slot was reused are unverified; the wait keeps polling and its queue stays blocked. Seen in several titles on a Windows NVIDIA GPU; not measured on hardware. -- [PM4 type-0 packets](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/CommandValidation.cpp) (libSceAgcDriver) - a dword whose packet type is 0, a zero header word included, is rejected with `unsupported PM4 packet type`, so the submission throws. Whether the console's command processor skips or executes such dwords, and what a run of them in front of a REWIND packet means, is unverified; shadPS4 (`liverpool.cpp`) and KytyPS5 (`pm4.cpp`) also treat type 0 as unsupported. One such submission begins with a seven-dword NOP and an eight-dword ACQUIRE_MEM, then 169 dwords of zero headers (dwords 15 to 183, zero in one run and stale words in another), and its packets start at dword 184, as sections of NUM_INSTANCES, SET_BASE, REWIND, NOP and ACQUIRE_MEM separated by zero gaps of 26 to 455 dwords (odd lengths included); letting a zero header through only moved the stop to a stale word. +- A [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) (libSceAgcDriver) on a queue 0x20 label can time out while the awaited word holds the address of another slot of the same label area instead of 0 or 1. On a Windows NVIDIA GPU the PM4 ring window (`APS5_PM4_RING`) held no store to that label. On a Windows AMD RX 6400 (PPSA01746) the address was already in the word when the wait began (6 of 6 timeouts) and a RELEASE_MEM storing 1 to the slot had been queued (5 of 5 with a record, another run), not necessarily landed. What writes the address and whether the console satisfies the wait are unverified; the queue stays blocked while the wait polls. Not measured on hardware. +- [PM4 type-0 packets](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/CommandValidation.cpp) (libSceAgcDriver) - a dword whose packet type is 0, a zero header word included, is rejected with `unsupported PM4 packet type`, so the submission throws. Whether the console's command processor skips or executes such dwords, and what a run of them in front of a REWIND packet means, is unverified; shadPS4 (`liverpool.cpp`) and KytyPS5 (`pm4.cpp`) also treat type 0 as unsupported. One such submission begins with a seven-dword NOP and an eight-dword ACQUIRE_MEM, then 169 dwords of zero headers (dwords 15 to 183, zero in one run and stale words in another), and its packets start at dword 184, as sections of NUM_INSTANCES, SET_BASE, REWIND, NOP and ACQUIRE_MEM separated by zero gaps of 26 to 455 dwords (odd lengths included); letting a zero header through only moved the stop to a stale word. Re-reading that submission (PPSA15246) from guest memory 0 to 2000 ms after the rejection changed at most two of its 6369 dwords and kept the number of non-zero dwords among dwords 15 to 183 at 21 (two runs). - [COND_EXEC](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) reads its condition after the earlier packets of its queue complete. Without PFP_SYNC_ME the console's graphics PFP reads it ahead of the ME, so a title that races the read sees a later value here. - A [flip inside a COND_EXEC range](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/CommandValidation.cpp) throws: the flip is reserved with the video output when the buffer is submitted, and how the console withdraws a skipped one is unknown. - A [COND_EXEC range](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) must hold whole packets and end inside its command buffer. A chained INDIRECT_BUFFER or a REWIND inside a range throws, as what the CP does with the skipped dwords then is unknown. @@ -161,6 +171,7 @@ - [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. - [DB_SHADER_CONTROL.STENCIL_TEST_VAL_EXPORT_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) becomes `FragStencilRefEXT` when available, taken from the second MRTZ channel of the 32_GR and 32_ABGR formats as Mesa's AMD drivers write it ([`emit_ps_mrtz_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L194)); the console was not measured. The host value is the test value of both faces and what `REPLACE_TEST` writes, so a stencil operation that writes all ones or the operation value throws with it, as do a stencil clear draw and the UINT16_ABGR layout. A device without the extension uses the bounded replay described above. On such a device a draw first drops the export when the reference cannot change its result (`StencilReferenceUnused`: no stencil test or attachment, or on both faces a compare that ignores it and no reachable `REPLACE` through a non-zero write mask), and its pixel shader is then compiled at its first draw, not when its ABI is resolved. A device with the extension keeps the export, so each such shader keeps one pixel variant. - [DB_SHADER_CONTROL.STENCIL_OP_VAL_EXPORT_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - the pixel shader supplies the stencil operation source value per pixel, in place of `DB_STENCILREFMASK.STENCILOPVAL`. Only the operations that take that value read it: `REPLACE_OP` and the clamped and wrapping add and subtract (codes 4, 5, 6, 8 and 9), and the logic operations, which throw already ([AMD_stencil_operation_extended](https://registry.khronos.org/OpenGL/extensions/AMD/AMD_stencil_operation_extended.txt), [AMD_shader_stencil_value_export](https://registry.khronos.org/OpenGL/extensions/AMD/AMD_shader_stencil_value_export.txt)). Vulkan has no per-pixel operation value. Without `STENCIL_TEST_VAL_EXPORT_ENABLE`, a `REPLACE_OP` reachable through a non-zero write mask (`StencilOperationValueReplaces`) becomes `REPLACE` with the exported value as `FragStencilRefEXT`, or the bounded replay without the extension; this requires a compare that ignores the test value and no `ONES` or `REPLACE_TEST` on either face, and early tests throw. The value is taken from bits 8-15 of the second MRTZ channel: the PPSA27836 pixel shader that uses it shifts it left by 8 before `exp mrtz` (`v_lshlrev_b32 v0, 8, v0`), which is the only evidence for the layout. The add and subtract operations, and `REPLACE_OP` with a test value export, throw. With the 32_GR or 32_ABGR Z format and no such operation, or without a stencil test, the export cannot change the result and is ignored. Other Z formats throw. The console was not measured. +- [Stencil logic operations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `STENCIL_NAND` and `STENCIL_NOR` (`DB_STENCIL_CONTROL` 13 and 14, [PS5_Mesa](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/registers/gfx103.json#L581-L598)) throw. AND, OR, XOR and XNOR (10, 11, 12 and 15) apply `STENCILOPVAL` as [AMD_stencil_operation_extended](https://registry.khronos.org/OpenGL/extensions/AMD/AMD_stencil_operation_extended.txt) defines them, not measured on the console, drawn as `ZERO`, `REPLACE` with those reference bits set, or `INVERT` under the write mask narrowed to the bits they change. A face whose other operations write other bits throws, as does OR on a compared test value bit it would set, or with a stencil test value export. - [DB_SHADER_CONTROL.DEPTH_BEFORE_SHADER](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `EarlyFragmentTests`, and so does `Z_ORDER` `EARLY_Z_THEN_LATE_Z` or `EARLY_Z_THEN_RE_Z` without kill, Z export, mask export or stencil export; with alpha-to-coverage the tests stay late. The meaning of the bit and of `Z_ORDER` 3 follows Mesa (`src/amd/registers/gfx10.json`; RADV sets the bit for `early_fragment_tests` in `src/amd/vulkan/radv_shader.c`). What the console does with `DEPTH_BEFORE_SHADER` and a Z export, a mask export, a stencil export or alpha-to-coverage together was not measured. - [PA_SC_CONSERVATIVE_RASTERIZATION_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 0x6001 uses Vulkan overestimation on devices that add at most 1/256 pixel and keep degenerate triangles; the console's uncertainty region and degenerate handling are unmeasured. A value with `OVER_RAST_ENABLE` and `UNDER_RAST_ENABLE` clear is decoded as disabled even when `UNCERTAINTY_REGION_MODE` or the `OUTER_`/`INNER_UNCERTAINTY_EDGERULE_OVERRIDE` bits (19:16) are set, as PPSA01474 writes 0xe6000; that these fields have no effect without over- or underestimation is assumed from their names, not measured. With `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` = 0 the `PERSP_CENTROID` and `LINEAR_CENTROID` inputs get the pixel-centre barycentrics, as without conservative rasterization ([shader debt](shader.md)): the one rasterized sample sits at the pixel centre, so every centroid the rasterizer could pick for an overestimated pixel is that point; that it does not move the centroid towards the primitive when the sample is uncovered is inferred, not measured. With more rasterized samples, and when a single-sample draw is raised to a multisampled color target, centroid inputs throw. - [Descriptor pool chain](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - each pool holds two storage buffer descriptors more than a set can take, as a workaround: RADV (Mesa 26.0 and later) lets a `FREE_DESCRIPTOR_SET` pool place a set up to 32 bytes past the pool's buffer ([`util_vma_heap_init`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_descriptor_pool.c) is given `bo_size + RADV_POOL_HEAP_OFFSET` as a size), and with a page-multiple pool size those bytes are an unmapped page @@ -172,13 +183,13 @@ - [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces still throw: hardware load bounds and filtered-sample addressing differ. A range of six faces or more that does not end on a whole cube keeps every slice up to `DEPTH`, the bound loads use on hardware; sampling the cube it cuts short is out-of-range cube-array sampling. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. - [Array layer views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (libSceAgcDriver) - `ArrayLayersServeView` lets a 2D texture at a 2D array image's base address, or a 2D array texture over its first layers, view that image (`Texture::CanCopyFrom`). Mesa-built homebrew (ps5-opengl) writes the view's last slice plus one to `CB_COLOR_ATTRIB3.MIP0_DEPTH`, so a slice-0 render target is a two-layer array image, and the title samples it as a plain 2D texture (SuperTuxKart PPSA69421 drew a black 3D scene and minimap without this). The per-layer layout of a mipmapped array is assumed equal to a standalone surface's; it is not measured on the console. A 2D texture at a later slice's address still gets an image of its own. - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. -- [Compressed FMASK reads](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) without an exact resident native owner throw. Native reads require eight fragments, one slice and mip, pipe-aligned DCC/CMASK, Z_64KB_X FMASK and unchanged, disjoint watched planes. The host stores independent samples and exposes their expanded indices (`0x76543210`, AMD PAL `c5e80007`, `Gfx9Fmask::GetPackedExpandedValue`); console equivalence is unmeasured. Other layouts, standalone compressed decoding, guest publication and recipe reuse remain unsupported. Metadata writes invalidate the native view, including same-value stores. +- [Compressed FMASK reads](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) without an exact resident native owner throw. Native reads require eight fragments, one slice and mip, pipe-aligned DCC/CMASK, Z_64KB_X FMASK and unchanged, disjoint planes, each watched or in a host import the Linux write-watch probe compares (kept as a copy and compared at most once per collect epoch); an FMASK import awaiting its Windows deferred read is read first (`WatchedAfterSettle`). The host stores independent samples and exposes their expanded indices (`0x76543210`, AMD PAL `c5e80007`, `Gfx9Fmask::GetPackedExpandedValue`); console equivalence is unmeasured. Other layouts, standalone compressed decoding, guest publication and recipe reuse remain unsupported. Metadata writes invalidate the native view, including same-value stores, except a same-value CPU store to a compared plane. - [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. - [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 DCC and FMASK contents are not interpreted. DCC over a multisampled target is accepted only in that pass and throws everywhere else. 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 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. - [DB depth copies](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp) (libSceAgcDriver) - with `COPY_CENTROID` the copied sample is sample 0, as [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state.c#L1679-1681) programs it under a full sample mask, so an edge pixel whose sample 0 the draw doesn't cover can differ from the console's first covered sample; not measured on the console. The copy runs as the title's draw with a driver-authored guest pixel program (hand-encoded RDNA2) that loads the sample through a driver-built T# and exports it as `32_R`. A multisampled plane is read without the native multisample import checks. Stencil copies, depth tests, planes other than `Z_32_FLOAT`, mips, a partial sample mask, blending, indirect draws and other targets throw. - [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off. PA_SC_AA_CONFIG is only compared with CB0 while PA_SC_MODE_CNTL_0.MSAA_ENABLE is set: PPSA08270 resolves a 4x 512x512 target with MSAA_ENABLE off and PA_SC_AA_CONFIG still at 2x (0x00108001) from earlier draws. @@ -192,6 +203,7 @@ - [HDR output override](../../../core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp) - setting `APS5_FORCE_SDR` suppresses HDR availability reported to titles while retaining the detected display and surface capabilities. It selects the existing SDR presentation path; it does not add support for floating-point BT.2100 PQ buffers (`0xC001070722000000` and `0xC001070700000000`), whose presentation remains refused. The override does not change the host display settings. - [DCC key stores](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp) cover the console's DCC extent only for pipe-aligned DCC of single-level, single-layer 4-byte SW_64KB_R_X surfaces; other surfaces, and every texture and color target key read, cover one byte per 256 bytes. - [DCC key fills over other images](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) - a uniform fill at a live image's `dccAddress`, at least one key per 256 bytes long, is a key fill of that image even when other live images overlap it, but only while it is no longer than the image's key extent (`DccKeyCount`, the pipe-aligned count); a longer one is classified by its overlaps as before. The bound is inferred, not measured on a title: a longer fill also writes the bytes past the keys, which the overlapping image may own. Whether the title's bloom-target key fills stay within the extent was not checked (`APS5_TRACE_DCC_KEYS` prints the length of the fills classified as keys, not of the refused ones). +- [Mipmapped DCC color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - the keys of a 2D color target with more than one mip are modeled for its whole surface only, as for a multi-mip storage image. The resident image takes a uniform clear over all of them, then marks them all uncompressed at its first write. The per-draw handling of a single-mip target's keys does not apply. A draw through the copied (non-resident) path leaves the keys as they were, and the CB metadata passes still refuse such a target. Keys that hold a different state per mip, and `ClearRegister` keys (whose value is a CB register), are not modeled for such a target. The keys of mipmapped DCC array and 3D targets are ignored. - [Comp-to-single DCC keys](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp) (libSceAgcDriver) - key 0x10 (`GFX9_DCC_CLEAR_SINGLE` in Mesa `ac_descriptors.h`) uniform over a surface makes every texel the element at the start of its 256-byte block, the top-left texel of the block where radv's `build_clear_dcc_comp_to_single_shader` stores the clear color. Sampled textures and storage images read that expansion (a storage image's first write stores the keys uncompressed, as under a clear code), and a CB DCC decompress pass stores it over the texels. A fast-clear eliminate over such keys throws, as a metadata pass over mixed keys does; color targets that render over such keys use their stored texels, and textures read mixed keys, and comp-to-single keys of a format whose element does not divide 256 bytes, as stored. "Uniform" is judged over the key bytes the driver reads, one per 256 surface bytes (see DCC key stores above), so a pipe-aligned surface whose keys differ past that range is expanded as if all were comp-to-single, as for the other clear codes. Not verified on hardware - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) on a device that cannot attach and blend R8_SRGB render into an RGBA8_SRGB copy of the target, so the sRGB encode, decode and blend rounding are the device's; the console's are not measured. - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. @@ -214,7 +226,7 @@ - [Packed 16-bit textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) - `5_6_5`, `1_5_5_5` and `4_4_4_4` are viewed as R5G6B5, A1R5G5B5 and B4G4R4A4 (the 4-bit format every Vulkan device can sample; R4G4B4A4 is optional and lavapipe lacks it) with a swizzle that reads X from the low bits, and a DST_SEL of the missing W of `5_6_5` reads X. Measured on RDNA2 (gfx1035), not on the console: through `image_load` and `image_sample_lz`, X, Y, Z (and W of `1_5_5_5` and `4_4_4_4`) come from the low, middle and high fields, and W of `5_6_5` returns X (the low 5 bits), not 1. Storage images use compatible R16_UINT views with the [buffer UNORM store conversion](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp); quantization uses nearest even and maps NaN to zero. Rounding ties and NaN handling for packed stores have not been checked on AMD or PS5 hardware. - [Min/max S# reduction](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FILTER_MODE` 1 and 2 use Vulkan's min and max reduction (the same `SQ_IMG_FILTER_MODE` values RADV programs). Measured on RDNA2 (gfx1035), not on the console: only texels of non-zero weight are reduced, and the weight is taken after rounding to the 8-bit subtexel grid (a sample within 1/512 texel of a centre reduces that texel alone); a driver with other `subTexelPrecisionBits` reduces a different set there. Gathers ignore the mode. - [Min/max S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver, shader recompiler) - throws with a linear mip filter, depth or color-texture compare, or bilinear or anisotropic filtering of an image the recompiler point-samples (sint, converted, depth-bits). With anisotropy the host sampler reduces over its own footprint; [RADV](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/amd/vulkan/radv_sampler.c#L185-L232) programs the same S# for such a Vulkan sampler. Measured on RDNA2 (gfx1030 in [#5156](https://github.com/boykopovar/AnyPS5/pull/5156#issuecomment-6155265616), gfx1035 in #5821), not on the console: without derivatives (`image_sample_lz`), min or max with anisotropic bilinear returns the bilinear minimum or maximum and with anisotropic point the point result, at every ratio field 0 to 4; with derivatives (`image_sample_d`, a 4:1 footprint), anisotropic bilinear at ratio field 2 or 4 reduces over the texels of all its anisotropic taps (the minimum or maximum along the major axis), not one bilinear footprint. A host that is not AMD reduces over its own anisotropic footprint, and Vulkan leaves nearest filtering with anisotropy to the host driver. -- [FORCE_DEGAMMA](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp) (libSceAgcDriver) - not measured on the console. A sampler that forces sRGB decoding throws for a format not measured on RDNA2. The rest follows gfx1035 and gfx1031 (point and bilinear): sRGB textures sample as without the bit, as in shadPS4 (PS4); UNORM `8`, `8_8`, `8_8_8_8`, BC1, BC2, BC3 and BC7 sample through their sRGB view (decoded before filtering), and SNORM `8`, `8_8` and `8_8_8_8` through the sRGB view of their bytes, as gfx1031 reads them; `16`, `16_16` and `16_16_16_16` UNORM, SNORM and FLOAT, `32`, `32_32` and `32_32_32_32` FLOAT, `10_11_11`, `2_10_10_10`, `5_9_9_9`, `5_6_5`, `1_5_5_5`, `4_4_4_4`, BC4, BC5 and BC6H sample as without the bit, as gfx1031 does. +- [FORCE_DEGAMMA](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp) (libSceAgcDriver) - not measured on the console. A sampler that forces sRGB decoding throws for a format not measured on RDNA2. The rest follows gfx1035 and gfx1031 (point and bilinear): sRGB textures sample as without the bit, as in shadPS4 (PS4); UNORM `8`, `8_8`, `8_8_8_8`, BC1, BC2, BC3 and BC7 sample through their sRGB view (decoded before filtering), and SNORM `8`, `8_8` and `8_8_8_8` through the sRGB view of their bytes, as gfx1031 reads them; `16`, `16_16` and `16_16_16_16` UNORM, SNORM and FLOAT, `32`, `32_32` and `32_32_32_32` FLOAT, `10_11_11` FLOAT, `2_10_10_10` UNORM, `5_9_9_9`, `5_6_5`, `1_5_5_5`, `4_4_4_4`, BC4, BC5 and BC6H sample as without the bit, as gfx1031 does. - [Tessellation factor ring](../../../core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp) (libSceAgcDriver) - `sceAgcDriverSetTFRing` records the ring and `sceAgcDriverGetTFRing` reports it. Before a title sets one, it reports the console's default, 0xFF0000000 and 128 KiB, as measured in [PS5_Vulkan's hardware findings](https://github.com/mihawk-99/PS5_Vulkan/blob/fde9e3798c2ec1d8fccc90d5067cd6060490607f/docs/HARDWARE_FINDINGS.md). Nothing is written to the recorded ring: tessellated draws run as Vulkan tessellation stages, which keep their factors on the host, so a title that reads tessellation factors from the ring finds none. A null base or a zero size throws; [ps5-opengl's AGC runtime](https://github.com/blackbearreloaded/ps5-opengl/blob/67c873f985eaefe0a72f713529e5632eaa01139c/src/platform/ps5_agc_native_runtime.c) calls `sceAgcDriverSetTFRing(0, 0)` in its cleanup, and what that does on the console is unknown. - [VertexBufferReadSize](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) and the [vertex staging copy](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - a vertex fetch whose index is at or past the descriptor's `num_records` no longer throws for `OOB_SELECT` 0 and 1. The copy still spans the whole fetch, but the bytes from `num_records * stride` onward read as zeros, so the out-of-range vertex is `(0, 0, 0, 0)`. Measured with `buffer_load_format_xyzw` (`idxen`) through the [hardware oracle](../HW_ORACLE.md) on RDNA2 gfx1036 for PPSA01325's word 3 `0x0004dfac` (`OOB_SELECT` 0) in #2241: indices 3, 4 and 5 over 3 records each return `00000000 00000000 00000000 00000000`, W included, unlike Vulkan `robustBufferAccess`, which returns `(0, 0, 0, 1)`. The zeros come from the record check, not from memory: stored values placed past the records are ignored, and a `num_records` 6 control returns them. `RESOURCE_LEVEL` 1, `OOB_SELECT` 1 and FORMAT 74 `32_32_32_FLOAT` give the same zeros, in wave32 and wave64; gfx1013 (the console) is not measured. `OOB_SELECT` 2 returns the stored values past the records and `OOB_SELECT` 3 compares `num_records` against the byte offset, both measured on the same setup in #2241; the two keep throwing past `num_records` here, since each needs its own rule. Zero-stride descriptors (`stride` 0) keep their separate byte-count check and are not covered here. Each out-of-range fetch is copied alone and outside the draw-snapshot cache, which is neither read nor written for it: a reused snapshot would carry another draw's bytes into the zero region, and a stored zero-padded copy would hand zeros to a later in-range draw at the same address. The valid prefix is still flushed for GPU writes before it is read. - [Draw snapshot buffer descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) rewrite each affected array from element zero, preserving unchanged descriptors. MoltenVK 1.4.2 writes auxiliary buffer lengths at index `i` instead of `start + i` for partial array updates ([MVKDescriptorSet.mm, BufferAuxSize](https://github.com/KhronosGroup/MoltenVK/blob/v1.4.2/MoltenVK/MoltenVK/GPUObjects/MVKDescriptorSet.mm#L1051)). Per-element writes therefore let one buffer length replace another even when Vulkan buffer handles and ranges are correct. @@ -229,7 +241,7 @@ - An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) T# whose surface has no mapped byte faults when selected instead of becoming a zero texture as a direct T# does; what the console returns for it is not measured. - [User-data callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is a user-data pair, or a copy of one made with `s_mov_b32` or `s_mov_b64` in the same block, calls the code at the address that user data holds. At dispatch the callee is read from guest memory up to its `s_setpc_b64` on the link pair, one copy of it is appended per call after the caller's last instruction, and the call is rewritten to `s_call_b64` to that copy. The words after the caller's last instruction are dropped from the recompiled code; in the shaders seen they are metadata or constants the caller reads relative to its program counter, and those reads go to the captured guest code, which linking does not change. A caller that jumps through a table among those words, which the control flow analysis reads from the recompiled code, throws. A copy runs at another program counter than the callee, so a callee that reads its program counter, makes a call, ends the program, jumps through another pair, returns before code it branches to, spans more than 64 KiB or is not readable throws. Callees are read again whenever a dispatch misses the dispatch cache; a dispatch that hits it reuses the linked program, so a title that rewrote a callee in place at the same address would keep running the old code. - [Table callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is two consecutive dwords of an `s_buffer_load_dword*`, or a copy of them made with `s_mov_b32` or `s_mov_b64` in the same block (a conditional branch forward past the call between them is crossed), calls one of the targets that buffer holds when the load's buffer descriptor comes from an `s_load_dwordx4`, or a 4-dword-aligned slice of an `s_load_dwordx8` or `s_load_dwordx16`, without SOFFSET from a user-data pointer, or from a pointer pair that two `s_mov_b32` of a literal or inline constant set, and the load's SOFFSET from an `s_lshl_b32` by 2 to 24 bits. The descriptor load may be in an earlier block, such as before a per-lane loop around the call, when it is the only write of the descriptor registers that reaches the buffer load on every path back from it, and no `s_call_b64` or statically resolved call lies on those paths; the paths may run through any `s_swappc_b64` that is not resolved statically, the table call itself included, whose callee is assumed to keep the descriptor registers. The chain compares the target each iteration actually loads, so a callee that changes them can only make a later target miss the chain and fault as an unlinked call. Paths follow fall-through, direct branches and the `s_setpc_b64` jumps whose target the control flow analysis resolves. A path that reaches, before a write of the descriptor registers, the program's first instruction, the entry of an in-program function (an `s_call_b64` target, whose callers are not followed), or an instruction that nothing modelled jumps or falls into fails the search. At dispatch the descriptor and the buffer are read from guest memory, and every non-zero 64-bit value at the loaded offset plus a multiple of the shift's stride, below the descriptor's size (stride times num_records, or num_records bytes with stride 0), is read as a user-data callee and appended once per call. The call becomes a branch to a chain that compares the source pair with each target (`s_cmp_lg_u32` with a literal and `s_cbranch_scc1` per dword) and `s_call_b64`s the copy that matches; no match runs the original `s_swappc_b64` as an unlinked call. There is one chain for each value SCC has at the call, and the chain sets SCC back to that value (`s_cmp_eq_u32 0, 0` or `s_cmp_lg_u32 0, 0`) before the callee or the unlinked call runs. Each copy follows its own compares and call, so no branch spans more than one copy. A later call's branch to its chain and its return cross earlier calls' copies through a pair of `s_branch` relays before the first of those copies and after each one, so they don't span more than one copy either. The searched range is the descriptor's whole size, which covers the range the load can reach whether it is checked in bytes or in strides; which one the PS5 uses for `s_buffer_load` was not measured. More than 64 distinct targets, a buffer above 64 MiB, or a call more than 0x7fff dwords before the end of the program and its user-data callees throws. As with user-data callees, the buffer is read again only when a dispatch misses the dispatch cache, so a target a title adds to the buffer later is an unlinked call when a cached program runs. -- [AGC FS table](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp) (libSceAgcDriver) - the driver maps 2 MiB at 0xF_E0000000 when it is created and fills the table that libSceAgc keeps at 0xF_E0040000 (six 16-byte V# slots, slot 0 describing eight 16-byte records) the way PS5 12.00 libSceAgcDriver and libSceAgc do, but every record points at one AnyPS5 routine that only returns (`s_setpc_b64` on the link pair). Sony's routines, which order work before a title's `ds_ordered_count`, are not modeled. +- [AGC FS table](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp) (libSceAgcDriver) - the driver maps 2 MiB at 0xF_E0000000 when it is created and fills the table that libSceAgc keeps at 0xF_E0040000 (six 16-byte V# slots, slot 0 describing eight 16-byte records) the way PS5 13.60 libSceAgcDriver and libSceAgc do, but every record points at one AnyPS5 routine that only returns (`s_setpc_b64` on the link pair). Sony's routines, which order work before a title's `ds_ordered_count`, are not modeled. - [DB_RENDER_CONTROL resummarize draws](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - HTILE is not modeled, so a RESUMMARIZE_ENABLE draw without depth or stencil tests and without color writes changes no modeled state and draws nothing. A pure resummarize with pass-through D16/D32 depth, no depth writes or bounds test, no stencil plane, no color writes and no running pixel stage also draws nothing; other resummarize, copy or decompress draws throw. - [Comparison-sampled R32 float and R16 unorm textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained. - [Null vertex buffer V#](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware. @@ -237,7 +249,8 @@ - [16-bit depth with stencil](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is stored as `D24_UNORM_S8_UINT`, or `D32_SFLOAT_S8_UINT`, on a device without `D16_UNORM_S8_UINT` as an attachment. Depth comparisons, clears and the depth image see the wider format's precision, so results at the 16-bit quantization boundary can differ from the console; the constant depth bias is scaled by 256 for D24, and a non-zero constant on the D32 float fallback throws. Not measured on console hardware. - [Depth surfaces created over storage writes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth surface created without HTILE copies its depth plane from the storage image at the plane's address, a pending one first, when that image has the plane's extent, one level, a 2D view or a 2D array view (its layer 0 is copied) and a 16-bit format for a 16-bit plane or a 32-bit format for a 32-bit plane. A multisampled surface, a surface the device stores in another format, a surface with HTILE and a surface without such an image start from `DB_DEPTH_CLEAR`, and the stencil plane from `DB_STENCIL_CLEAR`; CPU writes to the plane's memory are not read. Not measured on the console, where the plane is that memory. - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does, but only on a queue that has never delivered an interrupt from such a release: the queue is latched the first time a release interrupt reaches the event queue, and flips on that queue deliver nothing while its own release interrupts keep arriving. The latch is cleared when the queue has flipped five times without another interrupt from its own releases: the fifth flip delivers the event and unlatched the queue, so a title that waits on the flip event after latching recovers only if it submits further flips. The bound cannot be by time: the next release interrupt can arrive seconds later than a suppressed flip across loading stalls, and delivering an unexpected flip event on a queue that pairs flips with releases can crash; no fixed window short enough to unblock a waiting title keeps such a title alive. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; nor is how the console behaves for a title that waits on the flip event and also raises release interrupts on the same queue. -- [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 the larger of 2 GiB and half that heap (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). 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 last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. 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; 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: the use count stays below the first use at which an import that was neither evictable nor held could leave the 256-lookup protection, and either the build floor has not risen or no held import could leave it yet. 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 256-lookup protection, the 2 GiB reserve, the eighth, the 64 MiB steps and the three failures are choices, not measured. +- [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 the larger of 2 GiB and half that heap (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). 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 last 256 lookups; 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`. 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; 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: the use count stays below the first use at which an import that was neither evictable nor held could leave the 256-lookup protection, and either the build floor has not risen or no held import could leave it yet. 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 256-lookup 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. - [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. @@ -252,7 +265,7 @@ - [Viewport depth range remap](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - on a device without `VK_EXT_depth_range_unrestricted` a viewport depth range outside [0, 1] (Minecraft PPSA17221 draws its world with `PA_CL_VPORT_ZSCALE` 1, `ZOFFSET` 0 in negative-one-to-one clip space) is folded into the vertex shader: `RemapPositionDepth` scales clip z so z/w is the console's depth, the pipeline's depth clamp applies the console's `PA_SC_VPORT_ZMIN/ZMAX` clipped to [0, 1] (exact for a normalized depth target or none; Minecraft clamps its float depth target to [-1, 1], so a depth below 0 is stored as 0 there, which only a later test or read at exactly 0 can tell, and the first such draw is reported once), and two extra `gl_ClipDistance` elements restore the near and far planes depth clamping removes. The mode is a pipeline specialization constant (`PipelineSpecialization::DepthRemapBase`); scale and offset are read from the draw's push block (two words at byte 104, `DepthRemapPushOffsetBytes`, so a remapped draw's stage push data ends before it), so draws that differ only in them share one vertex module and one pipeline. The remap serves the vertex path only; mesh and tessellation draws with such a range still throw. Whether the console clamps before or after its depth bias and how it treats a clamp range narrower than the transform's are not measured. - [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 whose base array slice is past 0 is built from its viewed slices only (`ViewedArrayWindow`), at the guest address of its base slice. The window is taken only when the surface is thin, has no DCC metadata, and, for a 64 KiB XOR swizzle, when the base slice keeps every slice bit of the swizzle equation at zero (R_X 1-byte elements: a base slice that is a multiple of 16). Any other view still 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 the memory and uploads of both. PPSA26344 samples a 256x256 cube array at slices 4299..7232, built at 1467 MiB instead of about 3.8 GB. +- [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. - [2D reads of 2D arrays](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled image whose shader shape is 2D and whose T# is a 2D array is built from its base slice alone: the T# is narrowed to that slice (`FirstLayerDescriptor`) before its decode, cache key and image, in stage A records, stage B bindings, revalidation and image tables. A whole-array read of the same T# keeps its own cache entry, so both images exist when a title uses both. Storage images read or written through a 2D shape are not narrowed and still build every slice the T# names. PPSA26344 reads a 3328x1872 R_X T# of 1537 slices this way: the whole array was a 12.4 GiB image that did not fit the device-local budget of an RTX 4090. - [Sampled and storage views of 1 GiB or more](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) that stay whole are only logged (sampled views by `cachedTexture`, storage views by `cachedStorageTexture`), with the registered guest range holding their base and how many of their slices fit in it; nothing is capped to that range. PPSA26344 samples a 1467 MiB cube array window of which only 838 of 2934 slices lie inside its 3380 MiB range: the rest is read from whatever follows the range. Whether such slices are ever sampled is not measured. - [Color export type against the target format](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - the export-format check refuses a `UINT16_ABGR` export (`SPI_SHADER_COL_FORMAT` 7) into a target whose color-buffer number format is not unsigned integer, but not the reverse. A 32-bit export (`32_R`, `32_GR`, `32_AR` or `32_ABGR`) into an unsigned-integer target leaves `uintExport` clear, so the recompiler declares the fragment output `float4`; `DecodeColorBuffer` maps a number format of 4 to `R32_UINT` from the color buffer alone, independent of the export format, so the float output reaches an integer attachment. Desktop drivers keep the raw bits, but the Khronos validation layer reports `Undefined-Value-ShaderFragmentOutputMismatch` and MoltenVK's Metal compiler refuses the pipeline. Which export format a title pairs with a 32-bit integer target is not measured, so the output type is left following the export format rather than the attachment. @@ -260,6 +273,7 @@ - [Long Windows pipeline-cache paths](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp): `readHeader` and the failed-read existence check still use ordinary paths. A filename beyond MAX_PATH can prevent header reuse or be reported missing; the shared content reader and journal normalization do not cover these calls. - [DB_RENDER_CONTROL.STENCIL_CLEAR_ENABLE without a stencil plane](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): a stencil clear on a depth target whose `DB_STENCIL_INFO` format is `STENCIL_INVALID` is dropped; the depth clear and color writes proceed. AMD PAL ([`gfx9RsrcProcMgr.cpp`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32/src/core/hw/gfxip/rpm/gfx9/gfx9RsrcProcMgr.cpp)) and radeonsi (`si_clear.c`) never set the bit without a stencil plane, and shadPS4 drops it the same way (`vk_rasterizer.cpp`, `StencilValid`). No source states what the hardware does with the bit then; the console was not measured. A read-only stencil plane still throws +- [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. ### Opt-in draw paths diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index fa1c55df74..1e39166ff9 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -6,7 +6,7 @@ - [sceAjmInstanceExtend, sceAjmInstanceSwitch](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjm.native) - their four-argument signatures come only from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ajm.cs#L98-L104), whose comments do not describe them; shadPS4 [declares both without parameters](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ajm/ajm.h#L222-L223). Neither is measured on a console; these throwing declarations only resolve imports. - [sceAjmInstanceCreate](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a full 14-bit instance slot namespace returns `0x80930007` (`OUT_OF_RESOURCES`), as [shadPS4's InstanceCreate](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ajm/ajm_context.cpp) does for its own instance pool (PS4). The 16384 slots per codec prefix are this runtime's existing ID layout, not a measured console capacity; the PS5 capacity and exhaustion result are unverified. - [ATRAC9 vibration decoding](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): channel configurations 6 (mono) and 7 (dual mono) use the pinned [vgmstream decoder](https://github.com/vgmstream/vgmstream/blob/7c520e8f9b078dfde2f84f1b5bcbad0c6c415806/src/coding/libs/atrac9_dec.c), including its 12/24/48 kHz profile limits. Synthetic fixtures cover interleaving, reset after malformed input, partial superframes and PCM16/PCM32/float output; console PCM rounding and physical haptic output are unverified. Ordinary channel configurations and discrete multichannel streams still use LibAtrac9. Decoder creation and decoding run under the existing AJM instance lock because the dependency rebuilds shared Huffman tables during initialization. -- [sceAjmBatchJobGetInfo](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - writes the result and format sideband of a run job without buffers with the `SIDEBAND_FORMAT` flag (0x400000000000), as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does, not confirmed on a PS5 title; the format reports bitrate 0 for ATRAC9 as in KytyPS5, while [shadPS4](https://github.com/shadps4-emu/shadPS4) derives it from the superframe size; before the instance is initialized it returns the not-initialized result (1) and no format, like the other run jobs here, while KytyPS5 returns result 0 with the format of the uninitialized decoder +- [sceAjmBatchJobGetInfo](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - writes the result and format sideband of a run job without buffers with the `SIDEBAND_FORMAT` flag (0x400000000000), as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does, not confirmed on a PS5 title; the ATRAC9 format's bitrate is the superframe's bits per second (superframe bytes * 8 * sample rate / samples per superframe), as [shadPS4](https://github.com/shadps4-emu/shadPS4) derives it and as a PS4 reported it (168000 for 448-byte superframes at 48 kHz), where KytyPS5 reports 0; before the instance is initialized it returns the not-initialized result (1) and no format, like the other run jobs here, while KytyPS5 returns result 0 with the format of the uninitialized decoder - [sceAjmDecMp3ParseFrame](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: Layer I/II headers are accepted when no original-file-length (`parse_ofl`) lookup is requested, which is supported only for Layer III; MPEG-2.5 stops at 64 kbps. The `parse_ofl` layout (Xing/Info + LAME tag after side information, with the standard fixed Xing/Info offset retained for encoders such as [LAME](https://github.com/lameproject/lame/blob/master/libmp3lame/VbrTag.c#L854-L862), VBRI at byte 36 or after side information, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description. - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title (the one call in PPSA04354 passes a 24-byte result buffer, the 8-byte result plus the 16-byte ATRAC9 codec info written here; the call does not show the codec or the fields); a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts - [AJM Opus decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout and little-endian `u16` packet byte count come from The Smurfs Dreams (`2, 48000, 0`) and Lost Soul Aside (`4, 48000, 1`). The third word is interpreted as the mapping family, as in [KytyPS5's initialize structure](https://github.com/KytyPS5/KytyPS5/blob/984cfcebd141b211aa9e0a8cde63b0ebe652d07a/src/libs/ajm/opus_decoder.h#L11-L15). Family 1 uses the conventional stream counts, coupled counts and channel map from [libopus 1.5.2's surround layout](https://github.com/xiph/opus/blob/v1.5.2/src/opus_multistream_encoder.c#L47-L55), passed to FFmpeg through an [RFC 7845 section 5.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1) header with zero pre-skip and gain; gapless skipping remains controlled by AJM sideband parameters. FFmpeg returns PCM in its native speaker order. The implicit stream layout, PCM channel order and invalid family-0 channel count result are not verified on a console. NHL 27 (PPSA34063) passes `1, 48000, 255`. Family 255 with one channel is decoded as one mono stream, the same as family 0; this is assumed, since the stream count, coupled stream count and channel mapping that a family-255 Opus header carries ([RFC 7845 section 5.1.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1.1)) are not passed. Only 48000 Hz, mapping families 0 and 1, and family 255 with one channel are implemented; other rates and families, and family 255 with more channels, throw @@ -17,13 +17,21 @@ - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - initialize parameters are 8 bytes for ATRAC9 and M4AAC, 12 for Opus and none for MP3, as one title passes them; other codecs and sizes, and an output other than the 8-byte result, throw - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - SIDEBAND_GAPLESS_DECODE with RESET overwrites both counts as in [RPCSX](https://github.com/RPCSX/rpcsx); shadPS4 ignores a zero count when the other is nonzero. Without RESET they differ, so it throws - [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty +- [sceAjmBatchJobControlBufferRa, sceAjmBatchJobRunBufferRa, sceAjmBatchJobRunSplitBufferRa, sceAjmBatchStartBuffer](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - the batch layout (chunk kinds, 20-bit payload, flags bits 32-51 as the flags chunk's payload, the builders' chunk order) follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/854b291c63360ff24e590809aff54cf5ab2dd4c9/src/core/libraries/ajm/ajm_batch.cpp) and was confirmed byte for byte for 64-byte control and 80-byte run jobs by a PS4 trace of Knowledge is Power: Decades (CUSA12435, Wwise, 32 545 batches dumped by a GoldHEN plugin). The console's builders write only bits 0-3 (kind), bit 4 (a buffer chunk's output direction) and bits 6-25 (payload) of the header word and leave bits 5, 26, 27 and 31 as the previous batch left them, so the kind is read as 4 bits plus the direction bit, not as 6 bits +- [sceAjmBatchStartBuffer](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - the statistics instance 0x80000 and the inline chunk come from shadPS4 only: the traced title builds every batch with the builders and a return address, and no title was seen writing an inline chunk. Flags are kept as passed. Unknown chunk kinds, control flags other than RESET, INITIALIZE and SIDEBAND_GAPLESS_DECODE, control jobs with run buffers and run jobs with a control input throw +- [sceAjmBatchStartBuffer refusals](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - probe batches through the console's builders on a PS4 showed that a run buffer chunk of size zero is dropped whatever its address (the job runs as without it: 0x10 and nothing consumed for no room, 0 for no input), and that a null address with a nonzero size refuses the batch before any of its jobs runs, with 0x80930011 returned and the error record holding 0x80930016, the job chunk's address, the chunk's offset in the job and the job's return address. Control buffer chunks follow the same rule here, not measured. A size not a multiple of 8, a chunk that overruns its job or the batch, or a job without flags are refused the same way with 0x80930011 in the record; the console's report for those was not measured +- [sceAjmBatchStartBuffer null and empty batches](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - a null batch address returns 0x80930005 whatever the size, as the PS5 builders' sceAjmBatchInitialize does for a null buffer, and a batch of zero bytes at a non-null address is accepted, runs nothing and completes at once (a null address with size 0 is refused like any null address); neither was measured on a console (the probe that passed a null batch froze the console before a result was written, so it is not retried on hardware) +- [sceAjmStrError](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjm.native) - throws as unimplemented. The console's texts were read by calling its own export in PPSA01317 on a PS5 (firmware 13.40, reported on #3205): `ok` for 0, 24 lowercase texts without a full stop for 0x80930002 to 0x80930019 (`context does not exist` to `specified memory area is not registered`), and `(unknown)` for every other value including 0x80930000, 0x80930001 and 0x8093001a; none is implemented here. 14 titles and modules import it. Two were disassembled for this entry: the cri_ware_unity plugin of PPSA25936 calls it once (0x120929) after a negative Ajm result and passes the returned string with the code to a logging or formatting function; PPSA07426 calls it after a negative `sceAjmBatchWait` result (0xe863f0) and, after a success, for two job codes (0xe862f9 and 0xe86308) when the first is nonzero, and formats the strings into a text buffer. What those functions do with the strings was not followed, and the other importers were not read. +- [ATRAC9 run results](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - probe runs of a title's ATRAC9 stream through the console's builders on a PS4 showed the run result naming the side that ran out: `PARTIAL_INPUT` (0x8) when the input ran out with room left, on a superframe boundary or not; `NOT_ENOUGH_ROOM` (0x10) when the room ran out with input left, after any number of frames, and with nothing consumed for a zero room; both (0x18) when the input ended just as the output filled, which is every Wwise run job. ATRAC9 run jobs with `RUN_MULTIPLE_FRAMES` follow those rules here, vibration streams included, which were not traced; resampled runs and the other codecs keep the earlier rule (a trailing partial frame is partial input, a run that produced nothing is out of room) +- [ATRAC9 run without RUN_MULTIPLE_FRAMES](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the same PS4 probe decoded exactly one frame and reported 0 for such a run, and a job read from a PS4 batch buffer (sceAjmBatchStartBuffer) does the same here. A job from the PS5 builders still decodes its whole input, as FMOD in PPSA29141 needs (it puts two run jobs in one batch with the second's input right after the whole of the first's); whether a PS5 stops after one frame is not measured. MP3 and Opus run jobs stop after one frame on both paths. The console also rejected the tests' synthetic silent superframe after three frames (`CODEC_ERROR | FATAL`, internal result -577) where LibAtrac9 decodes four, so that vector proves job plumbing, not decoding; on the title's stream LibAtrac9 matched the console's PCM within 4 LSB, 83.5% of the samples exactly ### Functional -- [sceAjmDecWVorbisCreateHeaderPacket](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjmWVorbis, implemented in libSceAjm.native) - refuses an output smaller than the whole packet (56 bytes plus the setup packet) with `0x8A6E0003`. The PS5 12.00 bit writer only checks that each write starts inside the buffer, so the console can accept a buffer a few bytes short and write past it +- [sceAjmDecWVorbisCreateHeaderPacket](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjmWVorbis, implemented in libSceAjm.native) - refuses an output smaller than the whole packet (56 bytes plus the setup packet) with `0x8A6E0003`. The PS5 13.60 bit writer only checks that each write starts inside the buffer, so the console can accept a buffer a few bytes short and write past it - [AJM batch contexts](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): start, wait and cancel reject context IDs not allocated by initialization; batches belong to their creating context, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0ebea15c24253b60f514fc37a348dabc8d0bcb90/src/core/libraries/ajm/ajm.cpp). This is not verified on PS5. Finalization still does not invalidate contexts or release their instances and outstanding batches; jobs execute synchronously and cancellation does not interrupt them. +- [sceAjmBatchStart](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp): console results for malformed batch descriptors and corrupted jobs are unverified. A null batch buffer or an offset beyond its capacity returns the existing invalid-parameter error; a truncated or inconsistent private packed job throws before its payload is read. The packed representation belongs to this implementation. -- [ATRAC9 gapless segments](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a run job stops after the gapless total of output samples counted from the start of the segment, leaving the rest of its input unconsumed, and the next run starts the next segment with the decoder as it is and without a second skip. FMOD in PPSA29141 sets a total shorter than a sound's frames and reads the frames after it on later runs; with the output held at the total its stream reader waited for them forever and the last buffer repeated. The console's handling of the frames after the total is not documented; a reset (`SetGaplessDecode` with the reset flag, `ClearContext`, initialization) starts a new segment. An ATRAC9 run job without `RUN_MULTIPLE_FRAMES` decodes its whole input as one with it: FMOD in PPSA29141 puts two run jobs in one batch with the second's input right after the whole of the first's. Whether the console stops after one frame there is not verified; MP3 and Opus run jobs still stop after one frame +- [ATRAC9 gapless segments](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a run job stops after the gapless total of output samples counted from the start of the segment, leaving the rest of its input unconsumed, and the next run starts the next segment with the decoder as it is and without a second skip. FMOD in PPSA29141 sets a total shorter than a sound's frames and reads the frames after it on later runs; with the output held at the total its stream reader waited for them forever and the last buffer repeated. The console's handling of the frames after the total is not documented; a reset (`SetGaplessDecode` with the reset flag, `ClearContext`, initialization) starts a new segment. - [AJM M4AAC decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 2) - Ghost of Yotei (PPSA26344) initializes it through `sceAjmBatchJobControl`. The initialize parameters (`u32` config type, 1 ADTS or 2 raw, then `u32` sampling frequency index), instance flags (channels in bits 3-6, `ENABLE_SBR_DECODE` bit 32, `ENABLE_NONDELAY_OUTPUT` bit 33, surround interleave order bits 34-35), `{heaac, reserved}` codec info and decoder error result (`CODEC_ERROR | FATAL`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/ajm/ajm_aac.cpp) (PS4, fdk-aac) and are unverified on PS5; decoding uses FFmpeg's AAC decoder. Without `ENABLE_NONDELAY_OUTPUT`, the first two frames after initialization or reset are consumed without output. Raw input takes its channel configuration from the instance (0 means stereo) and is decoded as one access unit per run job. The format reports the last frame's bitrate. Invalid config type or raw sampling frequency index above 11 returns invalid-parameter result 4. The implementation throws for ADTS without a frame header, profiles other than AAC LC, multiple raw data blocks per ADTS frame, unsupported channel configurations, more channels than the instance supports, HE-AAC without `ENABLE_SBR_DECODE`, raw input with `RUN_MULTIPLE_FRAMES`, output room smaller than the previous frame length, and unknown codec flags. - [AJM](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix. - [AJM Opus mapping family 255 with several channels](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): the initialization words carry no stream count or channel mapping, so the decoder opens on the first packet. It counts the self-delimited streams of that multistream packet ([RFC 6716 appendix B](https://datatracker.ietf.org/doc/html/rfc6716#appendix-B)) and takes each stream's stereo flag from its TOC byte as coupled, then decodes with an identity channel mapping. A packet whose streams do not add up to the channel count throws. The layout is inferred, not read from the console; unverified on hardware. diff --git a/docs/dev/debt/libSceAjmi.md b/docs/dev/debt/libSceAjmi.md index d3ac224c47..0a5d293464 100644 --- a/docs/dev/debt/libSceAjmi.md +++ b/docs/dev/debt/libSceAjmi.md @@ -2,4 +2,4 @@ ### Unknown function info -- [libSceAjmi pending exports](../../../core/libs/prx/libSceAjmi/Export.cpp) - 5 named callable exports were identified in the PS5 3.20 export table and cross-checked against firmware 13.60. Signatures and behavior remain unverified; declarations use the unknown-signature convention and throw through `NotImplemented_nid_no_patch`. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAjmi.c). +- [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: `sceAjmBatchJobInlineBuffer` and `sceAjmBatchJobRunBufferRa`, the latter as `sceAjmBatchJobRunBufferRa_nid_disambig1`, which the NID patcher exports under the name's NID, because libSceAjm implements the same name for PS4 titles; a title importing it from libSceAjmi still gets the not-implemented error at call time. Signatures and behavior remain unverified; the declarations use the unknown-signature convention and throw through `NotImplemented_nid_no_patch`. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAjmi.c). diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index 1780d0bde0..1dbcd434cf 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -6,10 +6,11 @@ - [sceAppContentDownloadDataFormat](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - empties the `/download0` directory, creating it if needed. Imported by PPSA01317. A null mount point returns `PARAMETER` and another mount point returns `NOT_MOUNTED` as in `sceAppContentTemporaryDataFormat` and `sceAppContentDownloadDataGetAvailableSpaceKb`; the console's error codes for both and its behaviour with files still open are unverified -- [libSceAppContent pending exports](../../../core/libs/prx/libSceAppContent/Export.cpp) - 16 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppContent.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceAppContent pending exports](../../../core/libs/prx/libSceAppContent/Export.cpp) - 15 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppContent.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional - [Initialization](../../../core/libs/prx/libSceAppContent/Export.cpp) - service readiness and boot attributes are not modeled; successful initialization returns a zero boot record. Initialization-state guards, SDK 0x01500000 repeated-initialization errors and reserved-field checks are recovered. SDK-dependent service-error translation remains incomplete. - [Additional content](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries; `sceAppContentAddcontEnqueueDownload` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no download is ever queued; `sceAppContentAddcontDelete` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no additional content is ever installed. `sceAppContentGetAddcontDownloadProgress` answers `PARAMETER` for a null label or progress and `NOT_FOUND` otherwise, as [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2328-L2341) does for a label that is not installed additional content, and never writes the progress. Its 16-byte layout (`DataSize`, then `DownloadedSize`, both 64-bit) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L45-L54) and is unused here. The signatures of `sceAppContentAddcontEnqueueDownload` and `sceAppContentGetAddcontDownloadProgress` follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L140-L148), and the signature of `sceAppContentAddcontDelete` follows [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.h#L91-L92); dlc_emu is not a console measurement and no console source gives the error codes, so the signatures and the error codes are unverified +- [sceAppContentGetEntitlementKey](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - the service result without an entitlement is unverified: this model returns `DRM_NO_ENTITLEMENT` and leaves the 16-byte key unchanged. Initialization, null-pointer and SDK 0x01500000 label-padding checks match the reported [firmware 13.42 module probe](https://github.com/boykopovar/AnyPS5/pull/9638#issuecomment-6390573735), run in an emulator, not on hardware. That module forwards service labels 0, 7, 8 and 0xFFFFFFFF without checking them; this library ignores the service label. The system service's own validation and errors remain unknown. - [Temporary app-content storage](../../../core/libs/prx/libSceAppContent/Export.cpp) console behaviour and hardware return values are unverified, including behaviour with open files across unmount; host filesystem failures throw. Storage models one `/temp0` mount, preserving files on unmount while rejecting new path resolutions until remount, and leaving existing open host descriptors usable. `sceAppContentTemporaryDataMount` delegates to `sceAppContentTemporaryDataMount2` with option 1 (`TEMPORARY_DATA_OPTION_FORMAT`) to format on mount as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/app_content/app_content.cpp#L550-L553) does. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); pointer ABI follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). diff --git a/docs/dev/debt/libSceAudioIn.md b/docs/dev/debt/libSceAudioIn.md index abb06131ee..30e0e4df12 100644 --- a/docs/dev/debt/libSceAudioIn.md +++ b/docs/dev/debt/libSceAudioIn.md @@ -20,8 +20,8 @@ - [sceAudioInChangeAppModuleState](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInSetSparkParam](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInSetSparkSideTone](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature -- [sceAudioInSetCompressorPreGain](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature; the PS5 12.00 module returns 0 without reading any argument, and so does this one -- [sceAudioInSetNoiseGateThreshold](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature; the PS5 12.00 module returns 0 without reading any argument, and so does this one +- [sceAudioInSetCompressorPreGain](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature; the PS5 13.60 module returns 0 without reading any argument, and so does this one +- [sceAudioInSetNoiseGateThreshold](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature; the PS5 13.60 module returns 0 without reading any argument, and so does this one - [sceAudioInSetMicLevel](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInSetDeviceMuteStatus](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInSetDevConnection](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature @@ -40,5 +40,5 @@ ### Functional - [sceAudioInIsPadEmbeddedMic](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - returns 0 for every open port: on the console it returns 1 when the port captures from the controller's built-in microphone and 0 for another input device, but ports here capture from the host's default input device -- [sceAudioInHqOpen, sceAudioInAsyncOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) - PS5 wrapper validation requires 48000 Hz followed by exactly 128 samples for both calls; HqOpen then requires format 2. The host uses the same SDL capture path for both, so native asynchronous scheduling and high-quality processing are not reproduced. Common port validation, device selection and host capture pacing remain incomplete. +- [sceAudioInHqOpen, sceAudioInAsyncOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) - PS5 wrapper validation requires 48000 Hz followed by exactly 128 samples for both calls; HqOpen then requires format 2. The host uses the same SDL capture path for both, so native asynchronous scheduling and high-quality processing are not reproduced. Common port validation, device selection and host capture pacing remain incomplete. One title (PPSA03355, in its libfmod plugin) disassembled for this entry calls `sceAudioInAsyncOpen` from one call site (0xe5972) with index 0, 128 samples, 48000 Hz and a last argument of 2, plus a type argument of 0 or 1, which agrees with the 48000 Hz and 128 samples above; it stores the result in a slot of its own and branches on a negative result. What the last argument 2 means is not shown. - [sceAudioInGetGain, sceAudioInGetRerouteCount](../../../core/libs/prx/libSceAudioIn/Export.cpp) - GetGain writes 1.0f before handle validation and returns 0x80260105 for a null output, as confirmed by the PS5 module. Native handle bit checks and uninitialized/inactive port errors differ from the host port table. Native reroute counters follow device reassignment; the host opens each SDL device once and reports zero. diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index edab686a12..f35e437a3a 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -2,8 +2,9 @@ ### Silent stubs -- [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp) (libSceAudioOut) - returns 0, as the PS5 12.00 module (0x248d0) does whether or not the low-latency service runs. The module disconnects from the audio server and clears the service tables; here `sceAudioOut2LoInit` starts no service and open contexts and ports are left as they are +- [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp) (libSceAudioOut) - returns 0, as the PS5 13.60 module (0x248d0) does whether or not the low-latency service runs. The module disconnects from the audio server and clears the service tables; here `sceAudioOut2LoInit` starts no service and open contexts and ports are left as they are - [sceAudioOutMasteringTerm](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - returns 0 without doing anything: there is no mastering stage to tear down, and `sceAudioOutMasteringInit` keeps no state. Whether the console fails a Term without a matching Init is unknown +- [sceAudioOutSetMainOutput](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - returns 0 and does nothing, as the console function does on firmware 12.70: [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/docs/FINDINGS.md#L25) reads that export as `xor eax,eax; ret`. The export reads no argument, so its parameters stay unknown; other firmware versions are unverified ### Unknown function info @@ -13,15 +14,15 @@ - [sceAudioOutExOpen, sceAudioOutExConfigureOutput, sceAudioOutExClose](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - the HDMI bitstream calls follow [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/docs/FINDINGS.md#L31-L80), which disassembles firmware 12.70's libSceAudioOut and logs the calls on a console. `sceAudioOutExOpen(user, mode)` returns `0x80260015` for a mode above 10 or outside mask `0x61F`; otherwise it opens a port of internal type 6 in S16 stereo, 256 frames at 48000 Hz for modes 0, 1, 2 and 9 and 1024 frames at 192000 Hz for modes 3, 4 and 10. `sceAudioOutExConfigureOutput(control, flags, mode, target, option)` returns `0x80268009` when `control` is not 0, takes modes 0 to 10 and 0xFF and targets 1 to 3 and 0xFF, and returned 0 on the console; `sceAudioOutExClose` returned 0. The argument registers match [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L26-L28) and PPSA99051's [bitstream_out.cpp](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/sample-app/src/platform/ps5/bitstream_out.cpp#L29-L32). Audio reaches the host through SDL as PCM and the host has no HDMI bitstream output, so an Ex port has no host device: `sceAudioOutOutput` paces it at its own rate and drops its IEC 61937 bursts, and `sceAudioOutExConfigureOutput` changes nothing. Nonzero `flags` or `option`, other modes or targets, `sceAudioOutExClose` of a handle that is not an Ex port, and `sceAudioOutGetPortState` on an Ex port throw, since the console's answers are unknown - [sceAudioOutExGetMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) - native validation requires first argument zero, a non-null buffer and exactly 1024 bytes, in that order (errors 0x80260003/04/06); the second argument is ignored. The host still reports no audio descriptors by zeroing the record. Native content comes from monitor/device services and system audio configuration, which the SDL backend does not reproduce. - [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - the two 32-bit arguments and selector validation match the system library: selector 0 accepts outputs 0 through 2; selector 255 accepts outputs 1 and 2. No third latency argument exists. The host has no system 3D rendering stage, so the call still does not apply latency and does not reproduce the system initialization and output-availability checks. -- [sceAudioOut2MasteringInit and sceAudioOut2MasteringInitEx](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the initialized flag and the flag checks follow the PS5 12.00 module (0x22360): a repeated initialization returns 0, other flags return `0x80268201`, and InitEx also accepts bit 31, which the console passes to the audio server as a mastering mode. Nothing is sent to an audio server, and `sceAudioOut2MasteringInit` does not return the `0x80268006` the console returns before `sceAudioOut2Initialize`, which is a missing state check of its own. PPSA01289 calls Init twice before Term twice; by the module its second Term gets `0x80268203`, and the title only logs a non-zero result -- [sceAudioOut2MasteringGetState and sceAudioOut2MasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the state layout, the states ids 1 (default) and 2 (V2), the descriptor ids `0x01010001`, `0x01020001` and `0x01010003` and the error `0x80268204` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. GetState zeroes the states and writes the two descriptors; with no mastering stage the compressor and limiter values stay zero. SetParam accepts any parameter set and records nothing, as KytyPS5 does. The checks before them follow the PS5 12.00 module: both return `0x80268203` before mastering is initialized, GetState returns `0x8026800C` for a null state, SetParam returns `0x80268201` for a null parameter and `0x80268205` for flags above 1. The console's checks of `output` and `user` are not reproduced; both are ignored +- [sceAudioOut2MasteringInit and sceAudioOut2MasteringInitEx](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the initialized flag and the flag checks follow the PS5 13.60 module (0x22360): a repeated initialization returns 0, other flags return `0x80268201`, and InitEx also accepts bit 31, which the console passes to the audio server as a mastering mode. Nothing is sent to an audio server, and `sceAudioOut2MasteringInit` does not return the `0x80268006` the console returns before `sceAudioOut2Initialize`, which is a missing state check of its own. PPSA01289 calls Init twice before Term twice; by the module its second Term gets `0x80268203`, and the title only logs a non-zero result +- [sceAudioOut2MasteringGetState and sceAudioOut2MasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the state layout, the states ids 1 (default) and 2 (V2), the descriptor ids `0x01010001`, `0x01020001` and `0x01010003` and the error `0x80268204` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. GetState zeroes the states and writes the two descriptors; with no mastering stage the compressor and limiter values stay zero. SetParam accepts any parameter set and records nothing, as KytyPS5 does. The checks before them follow the PS5 13.60 module: both return `0x80268203` before mastering is initialized, GetState returns `0x8026800C` for a null state, SetParam returns `0x80268201` for a null parameter and `0x80268205` for flags above 1. The console's checks of `output` and `user` are not reproduced; both are ignored - [sceAudioOut2SetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - records the debug state id in the trace and returns 0, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) does; the host has no system mix to apply it to. Id `0x20` with a 4-byte param throws, and the not-initialized error is not returned. Not measured on a console - [sceAudioOut2GetSpeakerArrayCoefficients](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - validates the handle and the coefficient count like the ambisonics call, then writes the front pair at full gain and silence for every other speaker, ignoring the position, spread, height awareness and downmix spread radius: [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) and [prosper](https://github.com/mattias800/prosper/blob/main/prosper/src/hle/audio/hle_audio.cpp) both return that pair. The console's VBAP gains over the array's directions are not measured - [sceAudioOutSetMixLevelPadSpk](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - no source applies the level (shadPS4 only stores it); it is assumed to be a linear gain on the port's own output, and the -9 dB default (11626) is applied to every pad speaker port, including those of titles that never call the function - [sceAudioOutGetLastOutputTime](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - signature and behaviour taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [fpPS4](https://github.com/red-prig/fpPS4) reimplementations of the PS4 library, not confirmed on a PS5 title: the time is the process time at which `sceAudioOutOutput` or `sceAudioOutOutputs` accepted the port's last block, not the time the block reached the device - [sceAudioOutGetSystemState](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - the 32-byte layout (`float loudness`, 4 reserved bytes, 3 reserved 64-bit words) follows shadPS4's `OrbisAudioOutSystemState` and SharpProspero's `SceAudioOutSystemState` (`Size = 32`); it is not confirmed on a console. Loudness is reported as a constant 0.0f, although SharpProspero documents it in LKFS, so the value is unconfirmed. shadPS4 returns NOT_INIT before `sceAudioOutInit`; that check is not modelled. Imported by PPSA30528, which fails to load without it - [sceAudioOutSysOpen, sceAudioOutSysConfigureOutput, sceAudioOutSysClose](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99051 and PPSA99997, which fail to load without them; the signatures follow [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp#L24-L29), whose audio code both titles share, and are not confirmed on a console. `sceAudioOutSysOpen(user, mode)` opens a port only for mode 5, the TrueHD MAT route: [the same project's carrier table](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/iec61937.cpp#L125-L140), read from sceAudioOutExOpen's own tables in firmware 12.70, describes it as a 768 kHz port of 1024 S16 8-channel frames whose 16-byte frames are consumed at 192 kHz; every other mode throws. Audio reaches the host through SDL as PCM and the host has no HDMI bitstream output, so like an Ex port a Sys port has no host device: `sceAudioOutOutput` paces it at its own rate and drops the MAT bursts. `sceAudioOutSysConfigureOutput(type, flags, mode, target, option)` accepts only the observed `(1, 0, 5, 1, 0)` HDMI MAT switch and changes nothing; other arguments throw. `sceAudioOutSysClose` closes a Sys port and throws for any other handle. The restore path EVO-PLAYER-PS5 takes after a Sys port calls `sceAudioOutExConfigureOutput(0, 0, 0xFF, 0xFF, 0)`, so no Sys reset combination is known -- [sceAudioOut2PortCreate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - MAIN (type 0) format validation follows PS5 firmware 13.60 measurements: bits 8-11 accept 1, 2, 8 or 12, bits 0-6 accept 0 or 1, bit 7 requires 8 channels, and bits 12-31 do not affect validation. The sample types are still read as float and int16; bit 7's layout, the PCM effect of bits 12-31 and the port `flags` are unverified. Other port types retain unverified format validation, including 6 channels and rejection of bits 12-31 +- [sceAudioOut2PortCreate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - MAIN (type 0) format validation follows PS5 firmware 13.60 measurements: bits 8-11 accept 1, 2, 8 or 12, bits 0-6 accept 0 or 1, bit 7 requires 8 channels, and bits 12-31 do not affect validation. The sample types are still read as float and int16; bit 7's layout, the PCM effect of bits 12-31 and the port `flags` are unverified. Other port types retain unverified format validation, including 6 channels and rejection of bits 12-31. One title (PPSA03355, in its libfmod plugin) disassembled for this entry has three call sites (0xe5faf, 0xe64a3, 0xe6a3b): type 0 with format 0x800 at 48000 Hz and flags 0; type 0x100 with format 0x100 at 48000 Hz and flags 0, after which it compares the result with 0x80268002; and a third that was not read. The first format is within the MAIN ranges above; for type 0x100 the validation is unverified, and what the type means is not shown. - [sceAudioOut2PortCreate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - 12 channels are assumed to be L R C LFE Ls Rs Lb Rb Ltf Rtf Ltb Rtb as in KytyPS5's 12-to-8 fold, and 6 and 8 channels to be its first 6 and 8; no title or SDK header confirms the order - [sceAudioOut2PortCreate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - the stereo fold is assumed, not the console's downmix: C at -3 dB into both sides, Ls/Rs, Lb/Rb and Ltf/Rtf at -3 dB and Ltb/Rtb at -6 dB into their own side, LFE dropped - [sceAudioOut2PortSetAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - each PCM buffer set (attribute 0) is taken as one grain: buffers set between two pushes are queued and played one per grain, at most 16 queued. A grain with no new buffer repeats the last one only when the port has always been given that same buffer (rewritten in place); a port whose buffers move on plays silence instead. Inferred from PPSA12544, whose object port (type 6) is given one grain of a 64-grain ring per set, often two or three between pushes and then none for a few; the console behaviour for several sets per push and for a push without a new buffer is not documented @@ -57,7 +58,6 @@ - [sceAudioOutSetCaesarVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutSetHeadphoneOutMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutGetFocusEnablePid](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOutSetMainOutput](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutSetVolumeDown](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutSetChatBalance](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutSetRecMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature @@ -104,7 +104,7 @@ - [sceAudioOut2DebugStateCtrl](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2ContextBedWrite](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2PortCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2InitializeEx](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - the PS5 12.00 module (0x2a640) stores its 32-bit argument and runs `sceAudioOut2Initialize`; the argument's meaning is unknown and it is ignored +- [sceAudioOut2InitializeEx](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - the PS5 13.60 module (0x2a640) stores its 32-bit argument and runs `sceAudioOut2Initialize`; the argument's meaning is unknown and it is ignored - [sceAudioOut2Terminate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2PortRegister](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2PortUnregister](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index 66ebc2c6f3..f93a5065b4 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -2,7 +2,7 @@ ### Silent stubs -- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) lacks acoustic propagation: attributes have no effect, sources expose 36 initially empty, invalid path slots and no rays, rendering preserves the destination mix without adding path contributions, and allocated CPU work memory is mostly unused. Ray results and audio paths accept empty lists and throw otherwise; calculation returns initial native validation errors and throws for unsupported propagation. Room, portal, source and material handles are tracked, but destruction after their system is destroyed silently succeeds without native lifetime semantics being confirmed. Occlusion, reflections and reverb are absent. +- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) lacks acoustic propagation: attributes have no effect, sources expose 36 initially empty, invalid path slots and no rays, rendering preserves the destination mix without adding path contributions, and allocated CPU work memory is mostly unused. Ray results and audio paths accept empty lists and throw otherwise; `sceAudioPropagationSourceCalculateAudioPaths` returns the initial native validation errors, then goes through `NotImplemented_nid_no_patch`, as propagation is not implemented. Room, portal, source and material handles are tracked, but destruction after their system is destroyed silently succeeds without native lifetime semantics being confirmed. Occlusion, reflections and reverb are absent. ### Unknown function info diff --git a/docs/dev/debt/libSceAvPlayer.md b/docs/dev/debt/libSceAvPlayer.md index fb24c0d166..b248df63b3 100644 --- a/docs/dev/debt/libSceAvPlayer.md +++ b/docs/dev/debt/libSceAvPlayer.md @@ -2,8 +2,8 @@ ### Silent stubs -- [libSceAvPlayer](../../../core/libs/prx/libSceAvPlayer/Export.cpp): `sceAvPlayerSetLogCallback` accepts a callback that is never called, as the player produces no log messages, and `sceAvPlayerSetAvailableBandwidth` has no effect, as it governs HLS sources, which `sceAvPlayerAddSource` does not implement. -- [sceAvPlayerSetTrickSpeed](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time. +- [libSceAvPlayer](../../../core/libs/prx/libSceAvPlayer/Export.cpp): `sceAvPlayerSetLogCallback` accepts a callback that is never called, as the player produces no log messages, and `sceAvPlayerSetAvailableBandwidth` has no effect, as it governs HLS sources, which `sceAvPlayerAddSource` does not implement; after a source is added it returns `OPERATION_FAILED`, as [prosper's reading of the module](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/video/avplayer.cpp#L960-L972) describes. +- [sceAvPlayerSetTrickSpeed](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time. With a source, a zero speed returns `INVALID_PARAMS` before the `OPERATION_FAILED` of a stopped, finished or failed player, the order AnyPS5 had before; that order is unmeasured. In [prosper's reading of the module](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/video/avplayer.cpp#L929-L959), only the missing source is checked first. A zero speed is refused with `NOT_SUPPORTED`, with every other speed from -399 to 399 except 100, and no player state is checked. ### Unknown function info diff --git a/docs/dev/debt/libSceBgft.md b/docs/dev/debt/libSceBgft.md index 2240209a4d..95b8d7ac06 100644 --- a/docs/dev/debt/libSceBgft.md +++ b/docs/dev/debt/libSceBgft.md @@ -4,7 +4,7 @@ - [sceBgftServiceDownloadGetProgress](../../../core/libs/prx/libSceBgft/Export.cpp), `sceBgftServiceIntDownloadGetProgress`, `sceBgftServiceIntDownloadGetGameAndGameAcProgress` and `sceBgftServiceIntDownloadGetPlayGoProgress`: signatures and the shared 88-byte output are confirmed. The library forwards the output through IPC without accessing individual fields; their types remain unresolved. - [sceBgftServiceIntDownloadRegisterTaskByStorageEx](../../../core/libs/prx/libSceBgft/Export.cpp): confirmed parameter pointer and 32-bit task ID output. The input contains 32-bit scalars at offsets 0, 4, 56 and 104, strings at offsets 8, 16, 32, 40, 48, 64, 72, 80 and 88, and a 64-bit scalar at offset 96. The typed layout preserves the unused eight bytes at offset 24; scalar meanings and several string roles remain unresolved. -- [sceBgftServiceIntDownloadGetPatchProgress](../../../core/libs/prx/libSceBgft/Export.cpp) and `sceBgftServiceIntDownloadGetTaskInfo`: neither export nor a caller was found in the supplied decompiled library corpus. Their signatures remain unresolved. +- [sceBgftServiceIntDownloadGetPatchProgress](../../../core/libs/prx/libSceBgft/Export.cpp) and `sceBgftServiceIntDownloadGetTaskInfo`: both throw through `NotImplemented_nid_no_patch`; neither export nor a caller was found in the supplied decompiled library corpus. The `(const char* contentId, BgftProgress*)` and `(int32_t taskId, BgftTaskInfo*)` signatures follow the OpenOrbis headers, whose other Bgft prototypes match the confirmed ones here; they are not measured on a console. The task info layout is unknown. - [sceBgftServiceIntDebugPlayGoClearSetFreeZone](../../../core/libs/prx/libSceBgft/Export.cpp) (libSceBgft) - It is in the intersection of the export table of the console's libSceBgft (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/libSceBgft.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. It is declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceBgftServiceIntDebugPlayGoIsSetFreeZone](../../../core/libs/prx/libSceBgft/Export.cpp) (libSceBgft) - It is in the intersection of the export table of the console's libSceBgft (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/libSceBgft.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. It is declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index f0af2576a1..fc6344418b 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -2,21 +2,25 @@ ### Silent stubs -- [sceCesMbcsUcsContextInit](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) stores the profile pointer in the first 8 bytes of the caller's context; the console's context layout is not known. It returns the invalid-parameter error for a null context. The signature `int sceCesMbcsUcsContextInit(union* context, const union* mbcsUcsPrf)` follows the [Orbital IDA type database](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json). `sceCesRefersUcsProfileCp1251` returns a profile accepted by `sceCesSbcToUtf8` and `sceCesUtf8ToSbc` only, with the Windows-1251 table of Microsoft's [bestfit1251.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1251.txt), where byte 0x98 (unassigned) maps to U+0098 like the unassigned Cp1252 bytes; the console behavior for that byte is not verified -- [EUC-JP (CP51932) conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs): `sceCesUcsProfileInitEucJpCp51932` marks the caller's sheet with an 8-byte tag and returns it as the profile. `sceCesUtf8StrToMbcsStr`, `sceCesUcs2StrToMbcsStr`, `sceCesMbcsStrToUtf8Str`, `sceCesMbcsStrToUcs2Str`, their `Get*Len` forms and `sceCesEucJpToUcs2` convert ASCII, the JIS X 0201 half-width katakana (`0x8E` + `0xA1`-`0xDF`, U+FF61-U+FF9F) and the two-byte rows `0xA1`-`0xFE` for that profile, and every character for the CP1252 and CP1251 profiles. The two-byte table ([JisX0208.hpp](../../../core/libs/prx/libSceCesCs/JisX0208.hpp), 7336 of 8836 cells) is Microsoft code page 932 read through the standard JIS row/cell to Shift-JIS arithmetic, so it includes NEC row 13 and the NEC-selected IBM rows 89-92 and maps the Microsoft variants (U+FF5E at `0xA1C1`, U+2225, U+FF0D, U+FFE0-U+FFE2) instead of JIS X 0208's; that the console's CP51932 table equals it is assumed from the Microsoft code page, not measured. A character with several cells encodes to the lowest cell. A well-formed but unassigned cell answers `UNASSIGNED_CODE`, a trail byte outside `0xA1`-`0xFE` `INVALID_ENCODE`, a cut pair `SRC_BUFFER_END`; the console's errors for these are not known. Three-byte JIS X 0212 (`0x8F`) characters convert through a second table ([JisX0212.hpp](../../../core/libs/prx/libSceCesCs/JisX0212.hpp), 6067 of 8836 cells) read from the Unicode consortium's [JIS0212.TXT](https://unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/JIS/JIS0212.TXT); that the console's CP51932 table equals it is assumed from that mapping, not measured. A well-formed but unassigned three-byte cell answers `UNASSIGNED_CODE` and a cut triple `SRC_BUFFER_END`. A character with cells in both JIS X 0208 and JIS X 0212 encodes to the two-byte form, and one only in JIS X 0212 to the three-byte form. Signatures follow [orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json). The string forms stop at the source limit or at a zero code unit, which is neither converted nor counted, and write no terminator; `*Len` outputs count code units consumed and produced. A code point outside the BMP answers `UNASSIGNED_CODE`. Unverified on hardware. -- [Shift-JIS (CP932) conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs): `sceCesUcsProfileInitSJis1997Cp932` marks the caller's sheet with an 8-byte tag and returns it as the profile. `sceCesUtf8StrToMbcsStr`, `sceCesUcs2StrToMbcsStr`, `sceCesMbcsStrToUtf8Str`, `sceCesMbcsStrToUcs2Str`, and their `Get*Len` forms convert ASCII, JIS X 0201 half-width katakana (`0xA1`-`0xDF`, U+FF61-U+FF9F), two-byte Shift-JIS sequences (`0x81`-`0x9F` and `0xE0`-`0xFC` lead, `0x40`-`0x7E` and `0x80`-`0xFC` trail) mapped to JIS X 0208 row/cell indices against the CP932 table ([JisX0208.hpp](../../../core/libs/prx/libSceCesCs/JisX0208.hpp)), and the IBM extensions (`0xFA40`-`0xFC4B`, 388 characters) decoded to their corresponding JIS X 0208 code points. The user-defined area (`0xF040`-`0xF9FC`, which CP932 maps to Private Use Area U+E000-U+E757) and unassigned lead/trail combinations answer `UNASSIGNED_CODE`, an invalid lead or trail byte `INVALID_ENCODE`, a cut sequence `SRC_BUFFER_END`. Characters with multiple cells encode to the lowest cell. Signatures follow [orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json). Unverified on hardware. +- [sceCesMbcsUcsContextInit](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) stores the profile pointer in the first 8 bytes of the caller's context; the console's context layout is not known. It returns the invalid-parameter error for a null context and, as a payload process on a PS5 (firmware 13.60) did, for a null profile. The signature `int sceCesMbcsUcsContextInit(union* context, const union* mbcsUcsPrf)` follows the [Orbital IDA type database](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json). `sceCesRefersUcsProfileCp1251` returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with the Windows-1251 table of Microsoft's [bestfit1251.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1251.txt), except byte 0x98, which is unassigned +- [EUC-JP (CP51932) conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs): `sceCesUcsProfileInitEucJpCp51932` marks the caller's sheet with an 8-byte tag and returns it as the profile. Decoding follows the console (see the `sceCesEucJpToUtf8` entry), with the Microsoft code page 932 table read through the JIS row/cell arithmetic ([JisX0208.hpp](../../../core/libs/prx/libSceCesCs/JisX0208.hpp)); the console does not decode or encode JIS X 0212 either way (U+02D8 answered `0x805C0020`). Encoding takes the lowest cell of a character. Signatures follow [orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) +- [Shift-JIS (CP932) conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs): decoding matches a payload process on a PS5 (firmware 13.60) for every two-byte input through `sceCesMbcsStrToUcs2Str`, in result, lengths and output. `sceCesUcsProfileInitSJis1997Cp932` tags the caller's sheet and returns it as the profile. The string conversions decode ASCII, half-width katakana (`0xA1`-`0xDF`) and two-byte sequences (`0x81`-`0x9F` and `0xE0`-`0xFC` lead, `0x40`-`0x7E` and `0x80`-`0xFC` trail) through the CP932 table ([JisX0208.hpp](../../../core/libs/prx/libSceCesCs/JisX0208.hpp)), including the IBM extensions `0xFA40`-`0xFC4B`. The user-defined area `0xF040`-`0xF9FC` and unassigned cells answer `UNASSIGNED_CODE`, an invalid lead or trail `INVALID_ENCODE`, a cut sequence `SRC_BUFFER_END`. Signatures follow [orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) ### Unknown function info - [Unicode scalar conversions](../../../core/libs/prx/libSceCesCs/Unicode.cpp) (libSceCesCs) - console error precedence, optional length pointers and length units are unverified. Signatures follow the [Orbital type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); names are in the aerolib table pinned by `tools/nid_names.py`. Encoding follows [Unicode section 3.9](https://www.unicode.org/versions/Unicode17.0.0/core-spec/chapter-3/). Each call converts one scalar, including zero and U+FEFF, without a terminator or byte-order detection; lengths count bytes for UTF-8 and 16-bit units for UTF-16. Native forms use host byte order (supported hosts are little-endian). Errors follow the existing CES converters, not console measurements, and failure leaves outputs unchanged. No console or title comparison has been run -- [sceCesEucJpToUtf8](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the scalar conversion's errors and optional length pointers are unverified against the console. Its signature follows the Orbital type database linked above; it uses the existing CP51932 profile and JIS tables, with their mapping and profile-layout assumptions listed above, converts one character (including zero), and writes no terminator. Failure leaves the output and length pointers unchanged -- [Shift-JIS and EUC-JP scalar conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - errors, their precedence and the optional length pointers of `sceCesSJisTo*`, `sceCesEucJpTo{Utf16,Utf32}*` and `sceCes{Ucs2,Utf8,Utf16,Utf32}*To{SJis,EucJp}` are unverified against the console. Signatures follow the Orbital type database linked above; the CP932 and CP51932 table and profile assumptions above apply. A profile of another encoding answers `INVALID_PARAMETER`, a surrogate or a value above U+10FFFF `INVALID_ENCODE`, a code point outside the BMP `UNASSIGNED_CODE`. `sceCesSJisCodeToJisX0208` and `sceCesJisX0208ToSJisCode` do not check that the cell is assigned; their `INVALID_ENCODE` for rows or cells outside 1-94 and `INVALID_DST_BUFFER` for a null output are not console measurements -- [Strict JIS X 0208 profiles](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - cell `0x2140` and U+FF3C throw: JIS0208.TXT gives U+005C, glibc 2.39 and Python's `euc_jp` U+FF3C. In `sceCesUcsProfileInitSJis1997X0208`, bytes `0x5C` and `0x7E` and U+005C, U+007E, U+00A5 and U+203E throw: SHIFTJIS.TXT gives JIS X 0201 Roman, CP932 and Python's `shift_jis` ASCII; the console's is unknown. The table, [JIS0208.TXT](https://unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/JIS/JIS0208.TXT), is CP932's without rows 13 and 89-92 and with six cells changed, not measured. Per [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Text/Ces.cs#L57-L67), the `X0208Ss2` profile adds SS2 katakana and `X0208Ss2Ss3` SS3 JIS X 0212. Signatures follow Orbital; the conversions above take these profiles with the same errors, a missing SS2 or SS3 answering `INVALID_ENCODE` and Shift-JIS leads `0xF0`-`0xFC` `UNASSIGNED_CODE` +- [sceCesEucJpToUtf8 and sceCesEucJpToUcs2](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) with the CP51932 profile follow a payload process on a PS5 (firmware 13.60): every two-byte input and every `0x8F` three-byte input gives the console's result, length and output (the test pins a hash of both sweeps). Bytes below 0xA0 other than 0x0E, 0x8E and 0x8F convert to themselves; JIS X 0212 is not decoded, so `0x8F` forms return `0x805C0020` in rows X 0212 leaves empty and `0x805C0014` elsewhere; a null profile returns `0x805C0004`, and the lengths are cleared first and then set as the console sets them. Their signatures follow the Orbital type database linked above. The console reads any other profile pointer as a sheet, where these return the invalid-parameter error. +- [Shift-JIS and EUC-JP scalar conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - errors, their precedence and the optional length pointers of `sceCesSJisTo*`, `sceCesEucJpTo{Utf16,Utf32}*` and `sceCes{Ucs2,Utf8,Utf16,Utf32}*To{SJis,EucJp}` are unverified against the console. Signatures follow the Orbital type database linked above; the CP932 and CP51932 table and profile assumptions above apply. A profile of another encoding answers `INVALID_PARAMETER`, a surrogate or a value above U+10FFFF `INVALID_ENCODE`, a code point outside the BMP `UNASSIGNED_CODE`. UTF-8 input is decoded as `sceCesUtf8ToSbc` decodes it, so overlong forms answer `0x805C0015`. `sceCesSJisCodeToJisX0208` and `sceCesJisX0208ToSJisCode` do not check that the cell is assigned; their `INVALID_ENCODE` for rows or cells outside 1-94 and `INVALID_DST_BUFFER` for a null output are not console measurements +- [Strict JIS X 0208 profiles](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the tables were measured on a PS5 (`sceCesMbcToUtf32` and `sceCesUtf32ToMbc` over every one- and two-byte sequence and every scalar); the errors follow the other JIS profiles, a missing SS2 or SS3 answering `INVALID_ENCODE`. The table is CP932's without rows 13 and 89-92 and with seven cells changed (`0x213D` U+2014 and the six of [JIS0208.TXT](https://unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/JIS/JIS0208.TXT)). Cell `0x2140` is U+005C in `sceCesUcsProfileInitSJis1997X0208`, whose bytes `0x5C` and `0x7E` are U+00A5 and U+203E, and U+FF3C in the EUC-JP profiles, which keep ASCII and also encode U+00A5 and U+203E as `0xA1EF` and `0xA1B1`. Per [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Text/Ces.cs#L57-L67), `X0208Ss2` adds SS2 katakana and `X0208Ss2Ss3` SS3 JIS X 0212 - [UTF-16 and UTF-32 forms of the single-byte and MBCS string conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - errors, their precedence and the optional length pointers of `sceCesSbcTo{Ucs2,Utf16,Utf32}*`, `sceCes{Ucs2,Utf16,Utf32}*ToSbc`, `sceCesMbcsStr{To,Get}{Utf16,Utf32}*` and `sceCes{Utf16,Utf32}Str{To,Get}Mbcs*` are unverified against the console. Signatures follow the Orbital type database linked above. They follow `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the UTF-8 MBCS string forms with the profiles and tables listed above. A surrogate or a value above U+10FFFF answers `INVALID_ENCODE`, a valid code point no profile maps `UNASSIGNED_CODE`. The string forms stop at a zero code unit, write no terminator and count code units - [GBK (CP936) and EUC-CN conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesUcsProfileInitGbkCp936` and `sceCesUcsProfileInitEucCnGb2312` mark the caller's sheet with an 8-byte tag; the console's profile layout is not known. Both use one table ([Cp936.hpp](../../../core/libs/prx/libSceCesCs/Cp936.hpp)) read from Microsoft's [CP936.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT); the EUC-CN profile accepts only its `0xA1`-`0xFE` lead and trail bytes, so it includes the GBK cells inside that range and the CP936 forms of `0xA1A4` (U+00B7) and `0xA1AA` (U+2014) instead of GB 2312's. That the console's GB 2312 and CP936 tables equal it is not measured. User-defined areas and `sceCesGbUcsProfileSet*` are not handled. Errors, their precedence and the length pointers follow the Shift-JIS forms above and are unverified - [GB 18030 profiles](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the console's tables are not measured, and the sources disagree on 26 sequences. The 2000 profile follows ICU's `gb-18030-2000.ucm` at icu-data d9d3a6ed (before ICU-8427), which matches Python's codec: `0xA8BC` is U+E7C7 and `0x8135F437` U+1E3F. The 2005 profile follows `gb-18030-2005.ucm` (91850aec), which swaps those two and maps 24 PUA cells to U+FE10-U+FE19, U+9FB4-U+9FBB and six supplementary ideographs, keeping the PUA code points as encode fallbacks. Two-byte cells are CP936 plus [Gb18030.hpp](../../../core/libs/prx/libSceCesCs/Gb18030.hpp); four-byte runs include ICU's `gb18030Ranges`. The inits tag the sheet like GBK's; the `sceCesGb*`, `sceCesMbc*` and MBCS string forms accept them. The full mapping is assumed as the default of `sceCesGbUcsProfileSetUdaMapping` and `Set4ByteCharRange`, which throw. Supplementary characters to UCS-2 answer `UNASSIGNED_CODE`; other errors follow the GBK forms -- [sceCesRefersUcsProfileCp1250](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with the Windows-1250 table of Microsoft's [bestfit1250.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1250.txt), where the unassigned bytes map to their Latin-1 control characters as the Cp1252 and Cp1251 tables do; the console behavior for those bytes and the whole profile is not verified. PPSA34501 fails to start without this export (`qYu6y1E0Qbg`) -- [Generic MBCS character and Shift-JIS code helpers](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesMbcTo*` convert one character with any profile the MBCS context accepts, as the per-encoding forms above do. `sceCesSJisGetCode` and `sceCesSJisPutCode` read and write one raw Shift-JIS code (one byte for ASCII and `0xA1`-`0xDF`) without checking that it is assigned. `sceCesSJisCodeToJisX0213` and `sceCesJisX0213ToSJisCode` use the Shift_JIS-2004 arithmetic of JIS X 0213 annex 1 (plane 2 rows 1, 3-5, 8, 12-15 and 78-94 on leads `0xF0`-`0xFC`). `sceCesMbcsUcsContextInitCopy` copies the stored profile pointer. Signatures follow the Orbital type database linked above; errors, their precedence and the optional size pointers follow the other converters and are unverified against the console +- [sceCesRefersUcsProfileCp1250](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with the Windows-1250 table of Microsoft's [bestfit1250.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1250.txt), except bytes 0x81, 0x83, 0x88, 0x90 and 0x98, which are unassigned. PPSA34501 fails to start without this export (`qYu6y1E0Qbg`) +- [sceCesSbcToUtf8 and sceCesUtf8ToSbc](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) follow a payload process on a PS5 (firmware 13.60): the three tables match the console byte for byte, with Cp1252 bytes 0x81, 0x8D, 0x8F, 0x90 and 0x9D unassigned; a null profile returns `0x805C0004`; and the length and output are cleared first, as the console does. `sceCesUtf8ToSbc` decodes leads up to 0xFD (5- and 6-byte forms), returns `0x805C0015` for overlong forms and surrogates and reports the measured lengths. The console reads any other profile pointer as a table, where these return the invalid-parameter error; which of a null profile and a scalar above U+10FFFF it reports first is not measured +- [MBCS string conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesUtf8StrToMbcsStr`, `sceCesUcs2StrToMbcsStr`, `sceCesMbcsStrToUtf8Str`, `sceCesMbcsStrToUcs2Str` and their `Get*Len` forms follow a payload process on a PS5 (firmware 13.60) with the Cp1252 and EUC-JP profiles: the lengths are cleared and a terminator is written first, then the context, the source and the output are checked in that order; a source limit of 0 reads up to the terminating zero; the output keeps room for a terminator; and on an error the characters before it stay converted, terminated and counted. They decode UTF-8 as `sceCesUtf8ToSbc` does, and a UTF-16 surrogate answers `0x805C0015`. The UCS-2 forms and the other profiles were not measured beyond these cases. A zero-filled context converted nothing and returned 0 on the console, where this returns the invalid-parameter error +- [EUC-JP and Shift-JIS encoding](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) with the CP51932 and CP932 profiles gives the result, lengths and bytes a payload process read on a PS5 (firmware 13.60) for every UCS-2 value through `sceCesUcs2StrToMbcsStr` (the test pins a hash of both sweeps): the JIS forms U+00A2, U+00A3, U+00A5, U+00AC, U+2014, U+2016, U+203E, U+2212 and U+301C encode to the cells of their Microsoft forms, U+FEFF is consumed and writes nothing, EUC-JP writes U+0080-U+009F other than U+008E and U+008F as single bytes and answers `UNASSIGNED_CODE` for U+000E, and Shift-JIS answers `0x805C0024` for U+0080-U+009F. Decoding those bytes back is unchanged +- [Generic MBCS character and Shift-JIS code helpers](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - UTF-8/UTF-16 buffer errors and size pointers are unverified. Measured on a PS5: the single-character encoders (`sceCesUtf32ToMbc` over U+0000-U+110001 per profile, surrogates, a one-byte target) give 0x805C0015 for a surrogate, 0x805C0014 past U+10FFFF, the needed length with `DST_BUFFER_END` and 0 otherwise; a rejected decoded character (`sceCesMbcTo*` and each encoding's `To{Ucs2,Utf8,Utf16,Utf32}`, not `sceCesEucJpToUcs2`/`ToUtf8`) stores its length (the bytes before a NUL after the lead, the needed length when truncated, 1 for a bad lead, the sequence for a bad or unassigned trail), a zero UCS-2/UTF-32 character and no UTF-8/UTF-16 units. `sceCesMbcTo*` and `sceCes*ToMbc` take any profile the MBCS context accepts. `sceCesSJisGetCode`/`PutCode` are unchecked; `sceCesSJisCodeToJisX0213` and back use JIS X 0213 annex 1. Signatures follow Orbital - [Single-byte profiles](../../../core/libs/prx/libSceCesCs/SingleByte.hpp) (libSceCesCs) - the console's tables for these 27 profiles are not verified. Each returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with bytes 0x00-0x7F unchanged and the high half of a public table: ISO 8859-1, -2, -4, -5, -9, -10, -13, -14, -15 and -16 from [unicode.org](https://unicode.org/Public/MAPPINGS/ISO8859/), code pages 437, 737, 775, 850, 852, 855, 860-863, 865 and 866 from [Microsoft's PC tables](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/PC/), KOI8-R and KOI8-U from [unicode.org](https://unicode.org/Public/MAPPINGS/VENDORS/MISC/), and 1254, 1256 and 1258 from Microsoft's [best-fit tables](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/), whose unassigned bytes map to C1 controls as in Cp1250. Profiles whose public table leaves bytes undefined or maps them to private use still throw -- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 70 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. -- [EUC-KR, UHC and Big5 conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the tables, errors and Big5 duplicate choices are assumed, not measured. `sceCesUcsProfileInitEucKr`, `sceCesUcsProfileInitUhc` and `sceCesUcsProfileInitBig5Cp950` tag the caller's sheet like the EUC-JP profile. The tables are Microsoft's [CP949.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP949.TXT) and [CP950.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP950.TXT); EUC-KR is CP949 limited to bytes `0xA1`-`0xFE`. Bytes below `0x80` are ASCII; the KS X 1003 profile (`sceCes*UcsProfileSetSbcs`) is not implemented. User-defined areas (Private Use Area on Windows) answer `UNASSIGNED_CODE`. Big5 encoding takes the lower of two cells except for U+2550, U+255E, U+2561, U+256A, U+5341 and U+5345, as code page 950 does. The scalar calls to and from UCS-2 and UTF-8/16/32 convert one character as the Shift-JIS and EUC-JP scalar conversions do +- [sceCesRefersUcsProfileCp858](../../../core/libs/prx/libSceCesCs/SingleByte.hpp) (libSceCesCs) - the console's table is not verified. The profile is the Cp850 one with byte 0xD5 mapped to U+20AC instead of U+0131, which is the whole high half of ICU's [ibm-858_P100-1997.ucm](https://github.com/unicode-org/icu-data/blob/main/charset/data/ucm/ibm-858_P100-1997.ucm); bytes 0x00-0x7F stay unchanged as in the other single-byte profiles, not swapped as in IBM's control mapping +- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 61 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. +- [EUC-KR, UHC and Big5 conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the errors are assumed; the decode tables and the Big5 duplicate choices were measured on a PS5 (`sceCesMbcToUtf32` and each `ToUtf8` over every one- and two-byte sequence): UHC and Big5 match, EUC-KR apart from its single bytes. The tables are Microsoft's [CP949.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP949.TXT), plus `0xA2E8` U+327E from KS X 1001:2002, and [CP950.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP950.TXT); EUC-KR is CP949 limited to bytes `0xA1`-`0xFE`, below `0x80` ASCII; the KS X 1003 profile is not implemented. User-defined areas answer `UNASSIGNED_CODE`. Big5 encoding takes the lower of two cells except for U+5341 and U+5345, as a PS5's `sceCesUtf32ToMbc` does for every scalar (CP950 takes the higher cell of U+2550, U+255E, U+2561 and U+256A; the console does not). The profiles tag the caller's sheet like the EUC-JP profile diff --git a/docs/dev/debt/libSceComposite.md b/docs/dev/debt/libSceComposite.md index 45ba88b721..36d78197ce 100644 --- a/docs/dev/debt/libSceComposite.md +++ b/docs/dev/debt/libSceComposite.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceCompositorAllocateIndex, sceCompositorCheckCrash, sceCompositorCommandGpuPerfBegin, sceCompositorCommandGpuPerfEnd, sceCompositorCreateIndirectRenderTarget, sceCompositorDeleteIndirectRenderTarget, 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, sceCompositorSetIndirectCanvasCommand, sceCompositorSetIndirectCompositionCommand, sceCompositorSetIndirectRenderTargetConfigCommand, 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 +- [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 diff --git a/docs/dev/debt/libSceContentExport.md b/docs/dev/debt/libSceContentExport.md index 01f0d64324..e5d4a5e9ee 100644 --- a/docs/dev/debt/libSceContentExport.md +++ b/docs/dev/debt/libSceContentExport.md @@ -2,7 +2,7 @@ ### Functional -- [Content export sessions](../../../core/libs/prx/libSceContentExport/Export.cpp): the host has no media library, so `sceContentExportFromDataWithThumbnail` throws for a valid session without invoking data callbacks. Session allocation does not reproduce guest allocator calls, worker creation or their failure paths. `sceContentExportGetProgress` throws 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 `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. ### Unknown function info diff --git a/docs/dev/debt/libSceContentSearch.md b/docs/dev/debt/libSceContentSearch.md index 6df6de62ff..fe7b25931c 100644 --- a/docs/dev/debt/libSceContentSearch.md +++ b/docs/dev/debt/libSceContentSearch.md @@ -7,4 +7,4 @@ ### Functional - [Content queries](../../../core/libs/prx/libSceContentSearch/Export.cpp) use an empty host library and always report update id 0; the console's initial update id is unverified. No content rows are produced. Every title is assumed to have search permission. Nested filter/sort validation is incomplete: empty nested column sets, null filter values and unknown enum values throw. Guest heap allocation and the platform-dependent minimum heap size are unimplemented. Initialization and outer pointer/count/limit validation follow the supplied module. -- [Metadata access](../../../core/libs/prx/libSceContentSearch/Export.cpp) cannot open metadata by path or content id because missing-content errors remain unresolved. No metadata handles are issued. Queries reject invalid outer pointers but still throw for valid pointers because errors depend on the requested field; closing an unopened handle returns the confirmed invalid-argument error. +- [Metadata access](../../../core/libs/prx/libSceContentSearch/Export.cpp) cannot open metadata by path or content id because missing-content errors remain unresolved: past their argument checks, both opening calls go through `NotImplemented_nid_no_patch`. No metadata handles are issued. The field-info and value queries reject invalid outer pointers, then go through `NotImplemented_nid_no_patch`, as no metadata id is ever opened and the console's error for one depends on the requested field; closing an unopened handle returns the confirmed invalid-argument error. diff --git a/docs/dev/debt/libSceDiscId.md b/docs/dev/debt/libSceDiscId.md index 17eb02c445..8401e28ef4 100644 --- a/docs/dev/debt/libSceDiscId.md +++ b/docs/dev/debt/libSceDiscId.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceDiscIdGet](../../../core/libs/prx/libSceDiscId/Export.cpp) takes one output pointer; `sceDiscIdGet2` takes an output pointer and a 32-bit argument that must be zero. Both pass the pointer to a device ioctl without accessing its contents. Output structure sizes and fields require the device-side implementation or a caller and remain unresolved. +- [sceDiscIdGet, sceDiscIdGet2](../../../core/libs/prx/libSceDiscId/Export.cpp): `sceDiscIdGet` takes one output pointer; `sceDiscIdGet2` takes an output pointer and a 32-bit argument whose accepted values remain unresolved (it may be a bitfield). Both throw `NotImplemented_nid_no_patch` without reading or writing caller memory: the optical drive backend, the output structure layout, accepted flags and error returns remain unverified on hardware, so the unsupported behavior stays explicit. diff --git a/docs/dev/debt/libSceErrorDialog.md b/docs/dev/debt/libSceErrorDialog.md index 1166c74424..33a1e3ad3b 100644 --- a/docs/dev/debt/libSceErrorDialog.md +++ b/docs/dev/debt/libSceErrorDialog.md @@ -6,4 +6,4 @@ ### Unknown function info -- [sceErrorDialogOpenDetail, sceErrorDialogOpenWithReport](../../../core/libs/prx/libSceErrorDialog/Export.cpp) (libSceErrorDialog) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceErrorDialog.c); the no-parameter prototypes follow [shadPS4 `error_dialog.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ime/error_dialog.cpp#L167-L172) and are not measured on a console. They are declared without parameters solely to resolve known imports; this is not an ABI claim, and every call throws. +- [sceErrorDialogOpenDetail, sceErrorDialogOpenWithReport](../../../core/libs/prx/libSceErrorDialog/Export.cpp) (libSceErrorDialog) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceErrorDialog.c); the no-parameter prototypes follow [shadPS4 `error_dialog.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ime/error_dialog.cpp#L167-L172) and are not measured on a console. They are declared without parameters solely to resolve known imports. This is not an ABI claim. Both currently return `kErrUnexpectedFatal` (`0x80ED0004`); this return behavior is covered by unit tests but has not been independently verified on console hardware. diff --git a/docs/dev/debt/libSceFiber.md b/docs/dev/debt/libSceFiber.md index f1c33e85c3..da142ae02b 100644 --- a/docs/dev/debt/libSceFiber.md +++ b/docs/dev/debt/libSceFiber.md @@ -8,6 +8,6 @@ - [_sceFiberInitializeWithInternalOptionImpl](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - its signature and the flag values `0x1` (no ulobjmgr registration), `0x10` (context size check) and `0x100` (start with MXCSR `0x9fc0` and x87 control word `0x037f`) follow shadPS4's [initialize implementation](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L150-L153) and [flags](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.h#L27-L32). Not measured on a console. `0x1` has no effect because no ulobjmgr exists; other flag bits throw. shadPS4 also sets `0x100` for `build_version` 3.50 and later in every initialize; `opt_param` and `build_version` are ignored. - [_sceFiberAttachContextAndRun, _sceFiberAttachContextAndSwitch](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - signatures and behaviour follow shadPS4's [run](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L260-L261), [switch](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L336-L337) and [attach](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L46-L79) implementations, not measured on a console. A context is attached only to an idle fiber initialized without one; a fiber that already has a context, or is not idle, returns `SCE_FIBER_ERROR_INVALID`. A size that is not a multiple of 16 returns `SCE_FIBER_ERROR_ALIGNMENT` as in initialize, where shadPS4 returns `SCE_FIBER_ERROR_INVALID`. -- [Fibers without a context](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - running or switching to a fiber initialized without a context, before one is attached, throws. On the console such a fiber runs on the stack of its caller, per shadPS4's [run implementation](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L297-L313). +- [Fibers without a context](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - caller-stack execution and restart after yielding follow shadPS4 at commit `f68923fe807f657348e6719ce658bcbd0a2b1c72` ([run](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L238-L307), [switch](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L309-L360), [return](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L376-L401)), not measurements on a PS5. The original calling thread stack is reused even when switching from an attached fiber; contextless continuations are discarded. - [_sceFiberGetThreadFramePointerAddress](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber, NID `0dy4JtMUcMQ`) forwards to `sceFiberGetThreadFramePointerAddress`; that the argument list matches is inferred from the export without the underscore, as for the underscore Ult exports, not measured - [Fiber stacks](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - a fiber whose context is smaller than 64 KiB runs on an mmap'd host stack of its context size plus 1 MiB, so host calls made from it (21 KiB deep when PPSA01341 reaches SPIRV-Tools from a 16 KiB context) do not overwrite the contexts packed next to it; a larger context is the stack, as on the console. PPSA05684 needs the second: its job fibers have 64 KiB and 512 KiB contexts, the 512 KiB ones at 1 MiB-aligned addresses, and find their job record by masking the stack pointer (`~0xfffff` for the 512 KiB ones), which a host stack breaks (the game waits forever in GameInit, or faults later in a 64 KiB fiber). The 64 KiB limit is chosen between these two titles, not measured: a title with small contexts that also derives state from the stack pointer, or one whose host calls exceed what is left of a large context, is not covered diff --git a/docs/dev/debt/libSceFios2.md b/docs/dev/debt/libSceFios2.md index ac6b0499b6..7b43eb0959 100644 --- a/docs/dev/debt/libSceFios2.md +++ b/docs/dev/debt/libSceFios2.md @@ -2,4 +2,6 @@ ### 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, sceFiosDateFromComponents, sceFiosDateGetCurrent, sceFiosDateToComponents, 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, sceFiosIsValidHandle, 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 +- [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 diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 3158492e1f..13154d0ebc 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -2,15 +2,17 @@ ### Unknown function info + - [sceFontRenderCharGlyphImage, sceFontRenderCharGlyphImageHorizontal, sceFontRenderCharGlyphImageVertical](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) accept a surface whose width or height is 0, with any row stride, and return the glyph metrics and an empty update rectangle without writing to the surface. A negative width, height or stride, a zero stride on a surface with pixels, a null buffer and a pixel size other than 1 or 4 bytes still return `SCE_FONT_ERROR_NO_SUPPORT_SURFACE`; those cases and the exact console result for a zero-extent surface are not measured. The extent-0 behaviour is inferred from the Tekken Dark Resurrection PSP emulator which, to measure a string, initialises the surface with width 0 and height 31 and an unbound stride, renders each character and sums the returned advances. +- [sceFontCreateString](../../../core/libs/prx/libSceFont/src/Text.cpp): a string created from a default-form (0x10) text source gets the direction of its first strong character, 0x11 for right to left and 0x12 for left to right, and 0x12 when there is none; explicit 0x11 and 0x12 sources keep their form and [sceFontStringGetWritingForm](../../../core/libs/prx/libSceFont/src/Text.cpp) returns the stored form. LRM and RLM count as strong, embeddings and overrides do not, and text between an isolate initiator and its PDI is skipped. The rule follows a run of the firmware 13.42 library in a harness reported on the pull request: for non-empty text it was checked before a later glyph-stage failure and only empty text completed, so it is not full native right-to-left rendering. Which characters are strong is decided here by Unicode block ranges, not by the full bidi class tables. - [Typographic design](../../../core/libs/prx/libSceFont/src/Style.cpp): native selector, feature, font-set eligibility and output-clearing rules are recovered. Driver typography selection and queries, feature-cache updates and glyph substitution remain unimplemented and throw instead of storing arbitrary selector/value pairs. - [sceFontSetScriptLanguage and sceFontGetScriptLanguage](../../../core/libs/prx/libSceFont/src/Style.cpp): native script/language ranges, handle-error precedence and clearing of failed getter outputs are implemented. Valid driver queries and updates throw; the previous arbitrary-value map and fabricated zero default are no longer used. Driver selection and actual language-dependent shaping remain unimplemented. - [sceFontGetRenderScaledKerning](../../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - arguments, the cleared output and the bound renderer check taken from `cellFontGetRenderScaledKerning` in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellFont.cpp) (PS3 predecessor of the library), not confirmed on a title. The kerning is the `sceFontGetKerning` one at the render scale - [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. -- [sceFontWritingRefersRenderStepCharacter](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the second argument is an out pointer to the writing's letter step, as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1882-L1896) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings. Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): exactly 8 bytes are written, the address of a step stored inside the writing, and a null writing writes null; shadPS4 and prosper treat the argument as a struct, which the measurement contradicts. The letter step lives in the writing, so the pointer stays valid as long as the writing does, and its contents are rebuilt by each call of this function. On the PS4 library repeated calls on one step walk the characters it covers and then return null; here every call returns the step's one character, so the second call differs. Assumed: each step covers one character and one glyph. PPSA06699 (Hot Shots Golf 2) dereferences the returned pointer for its character counts. +- [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 @@ -19,14 +21,17 @@ ### Functional -- [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the other script files of a set are chosen from its type, not measured on a console: Vietnamese for low-byte bit 0x10, Thai for script bit 0x10, Japanese for 0x04, simplified Chinese for 0x80 (before Japanese with 0x08), Hangul for 0x20 and Arabic for low-byte bit 0x80, as the SDK set names (`..._JP_AR`, `..._GBCJK_TH`) listed by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/font/font_internal.cpp) suggest; the file order and the Hangul file name (`SCEPS4Yoongd-Medium.otf`, `SCEPS4Yoongd-Bold.otf`) also come from shadPS4. A character the primary file lacks is taken from the first of them that has it; kerning between two files is 0, and `sceFontGetCharGlyphCode` and `sceFontGetFontGlyphsCount` answer for the primary file only +- [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the other script files of a set and their priority are inferred, not measured on a console: Vietnamese for low-byte bit 0x10, Thai for script bit 0x10, Japanese for 0x04, simplified Chinese for 0x80 (before Japanese with 0x08), Hangul for 0x20 and Arabic for low-byte bit 0x80. The SDK set names (`..._JP_AR`, `..._GBCJK_TH`) and Hangul aliases (`SCEPS4Yoongd-Light.otf`, `SCEPS4Yoongd-Medium.otf`, `SCEPS4Yoongd-Bold.otf`) come from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b45b289761ffcb4feed44df5f51b4bb5b5c0dfdc/src/core/libraries/font/font_internal.cpp); the Vietnamese bit rule and the priority used here are our inferences, not its fallback implementation. A character the primary file lacks is taken from the first of them that has it; kerning between two files is 0, and `sceFontGetCharGlyphCode` and `sceFontGetFontGlyphsCount` answer for the primary file only - [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans Arabic, Noto Sans CJK with its Korean face for Hangul) is loaded instead; its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. - [sceFontGetCharGlyphMetrics, sceFontGetRenderCharGlyphMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) report the vertical metrics the way the [FreeType driver](../../../core/libs/prx/libSceFontFt/src/Driver.cpp) reports them for a rendered glyph, not confirmed on a title: FreeType's vertical advance, and for a face without vertical metrics a bearing of half the horizontal advance left of the horizontal bearing and of half the advance left over by the glyph height. +- [sceFontGetFontMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - the 64-byte output, the form fallbacks (2 and 3 fall back to the `hhea` values when OS/2 is missing or gives an empty range, 4 builds vertical values from `vhea` or OS/2) and the raw weight deltas and slant copied from the style are not measured on a console. For a font set the values are those of its primary file; which file the console reports is not recovered. Closes when: measured on a PS5 +- [sceFontGetFontStyleInformation](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - the 24-byte output (FreeType family and style names, italic and bold flags, OS/2 weight class when it is a multiple of 100 from 100 to 900, width class below 10, and 1 plus a right-to-left `head` direction plus 0x80 for vertical metrics) and the font-set record path are not measured on a console. A face without a `head` table, such as a BDF font, throws, as its console result is unknown. Closes when: measured on a PS5 +- [sceFontGetFontGlyphsOutlineProfile](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - the 16-byte output (the `maxp` maximum contours, points, composite contours and composite points) and `SCE_FONT_ERROR_NO_SUPPORT_FUNCTION` for a `maxp` older than 1.0 are not measured on a console. A handle with a font-set record throws, as the maximum over the record's system font entries is not modelled. A face without a `maxp` table throws, as its console result is unknown. Closes when: measured on a PS5 - [Font writing lines](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): visual ordering and spacing originated from PS4 measurements and match the supplied PS5 structure and control flow. Exhaustive floating-point equivalence, including NaN payloads and rounding, has not been verified against a running PS5 module. -- [Font graphics and metric signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): graphics-device/service creation takes font memory, parameters and a handle output; destruction takes a handle pointer. Attribute selectors and outputs are 32-bit values. Metric, outline-profile, style-information and expansion-buffer outputs remain opaque pending field recovery. Protected implementations are unchanged. +- [Font graphics and metric signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): graphics-device/service creation takes font memory, parameters and a handle output; destruction takes a handle pointer. Attribute selectors and outputs are 32-bit values. Expansion-buffer outputs remain opaque pending field recovery. Protected implementations are unchanged. -- [Font graphics fill and transform signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): region layout and scalar float arguments are confirmed from assembly, including the extra 32-bit corner flags omitted by the roundish-region decompilation. Fill-method, plot and rates layouts, layout selector meanings and mapping parameter meanings remain unresolved. Protected bodies are unchanged. +- [Font graphics fill descriptors](../../../core/libs/prx/libSceFont/src/Graphics.cpp): the region, fill-method, fill-plot and fill-rates initialisers and setters, and `sceFontSetGraphicsErrorCallback`, follow firmware 13.60 `libSceFont.sprx` (read from disassembly, not measured on a console): argument checks in the same order, error codes, tags and the bytes written. What the layout selectors, mapping values, region corner flags and fill effects mean is unknown, because the calls that read these descriptors (`sceFontGraphicsSetup*`, `sceFontGraphicsUpdate*`) still throw. The error codes `0x80460086` to `0x80460089` have no public names; the source names them after the argument they reject Frame lifecycle, drawing completion, device usage, frame policy, clipping and fill setup/update argument signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered. Frame and resource layouts remain opaque; their runtime implementation is still unavailable. @@ -39,3 +44,4 @@ Canvas, design, surface texture and fill-pattern construction signatures in [Uni The [glyph support registration and texture-image fill signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered from native argument use. The image constructor takes a byte span and four scalar coordinates; the decompiler omits its successful path. `CreateWords` and `CreateGraphicsServiceWithEdition` consume the output pointer in the fourth general-purpose argument slot but do not read the third slot, whose source type is not established. - [sceFontCreateGraphicsServiceWithEdition and sceFontCreateWords](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) take their handle output in the fourth integer argument slot. The third slot is not read by the supplied module and is represented as an opaque 64-bit value; its declared source type remains unknown. +- [Word dictionaries](../../../core/libs/prx/libSceFont/src/Words.cpp): building, depth limit, sort and error rules and longest-match selection are read from disassembly, not measured on a console. The text source is parsed once instead of twice. More than 65535 nodes return `SCE_FONT_ERROR_FUNCTIONAL_LIMIT`, where the native 16-bit indices would wrap. The shape-entry test on the next character is skipped since strings built here have no shape entry. The low flag byte and flag bits 43-45 are tested as read; strings built here leave them clear, so names are ours. diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 5fd44fd1d1..205ee0df69 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -1,6 +1,9 @@ # libSceFreeType technical debt ### Unknown function info +- [FT_New_Size, FT_Activate_Size and FT_Done_Size](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures, errors and generic-finalizer ordering are unverified; the LP64 guest adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Private size fields are bridge-owned. Sizes created outside the bridge throw when selected. + +- [FT_Reference_Face](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signature, reference-counter limits and finalizer behavior are unverified; the LP64 guest adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Counter overflow and reentrant finalization have not been measured on the console. - [FT_Get_Sfnt_Name_Count, FT_Get_Sfnt_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, record layout and errors are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftsnames.c) and assumes the public LP64 `FT_SfntName` layout (24 bytes, string pointer at 8, byte length at 16). Strings retain their original encoding, borrow native face storage and remain valid only while the face is alive @@ -8,12 +11,18 @@ - [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_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 or signed 64-bit overflow in scaling, translation or midpoint sums throw; native behavior for that arithmetic is undefined. Callers must supply valid point/tag/contour arrays and the callbacks their contours use. + - [FT_Init_FreeType, FT_New_Face, FT_Set_Pixel_Sizes, FT_Get_Char_Index, FT_Load_Glyph, FT_Render_Glyph](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the PS4 library is taken to be FreeType 2 built for LP64: the face, size, charmap, glyph slot, bitmap and outline layouts follow the public headers of the bundled FreeType 2.13.3 with a 64-bit `long`, which matches the offsets the OpenOrbis samples read (`face->glyph` at 0x98, `slot->advance` at 0x80, `slot->bitmap` at 0x98, `slot->bitmap_left` at 0xc0). Which FreeType version and options the console library is built with is unknown, so the behaviour and error codes are those of FreeType 2.13.3. The OpenOrbis font, keyboard, threading, system and SDL2 samples import these six functions - [FT_Bitmap_New, FT_Bitmap_Copy, FT_Bitmap_Convert, FT_Bitmap_Done](../../../core/libs/prx/libSceFreeType/src/Bitmap.cpp) (libSceFreeType) - console behaviour and error codes are unmeasured; these follow bundled FreeType 2.13.3. Copy and Convert throw when source and target share a non-null buffer; Convert also throws when they are the same object. Other overlapping storage remains forbidden by the native API contract - [FT_Request_Size](../../../core/libs/prx/libSceFreeType/src/SizeRequest.cpp) (libSceFreeType) - console signature, request layout and errors are unverified; the guest request follows the LP64 public headers of the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/include/freetype/freetype.h). Host overflow behavior remains that of the bundled build. On Windows, native `FT_Long` is 32 bits; valid requests whose width or height exceeds that range throw before changing the size. +- [FT_Select_Size](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures and errors are unverified; the LP64 guest adapter follows the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Bitmap-strike selection and size metrics are checked against native FreeType with generated BDF fonts; other font drivers have not been exercised through this entry point. + - [FT_New_Memory_Face, FT_Done_Face and FT_Done_FreeType](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures, errors and generic-finalizer ordering are unverified; they follow the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/include/freetype/freetype.h) with LP64 guest arguments. Caller-owned font bytes must remain valid until the face is freed. - [FT_Outline_Check, FT_Outline_Get_CBox](../../../core/libs/prx/libSceFreeType/src/Outline.cpp) (libSceFreeType) - console signatures, count signedness and errors are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c), including its unsigned 16-bit counts and contour endpoints. Control-box coordinates use the existing guest LP64 layout and retain their full 64-bit range on Windows; contour checking does not validate point tags diff --git a/docs/dev/debt/libSceFreeTypeOptOl.md b/docs/dev/debt/libSceFreeTypeOptOl.md new file mode 100644 index 0000000000..ea253fc9c5 --- /dev/null +++ b/docs/dev/debt/libSceFreeTypeOptOl.md @@ -0,0 +1,9 @@ +# libSceFreeTypeOptOl technical debt + +### Unknown function info + +- [FT_Get_PFR_Advance, FT_Get_PFR_Kerning and FT_Get_PFR_Metrics](../../../core/libs/prx/libSceFreeTypeOptOl/Export.cpp) use the LP64 face adapter and bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftpfr.c) through [Pfr.cpp](../../../core/libs/prx/libSceFreeType/src/Pfr.cpp). A null face returns Invalid_Argument (6) without writing outputs or calling host imports, matching [FreeType 2.4.11](https://github.com/freetype/freetype/blob/VER-2-4-11/src/base/ftpfr.c) and [BunnyFl's firmware 13.42 Unicorn report](https://github.com/boykopovar/AnyPS5/pull/9646#issuecomment-6405909027). That report attributes a 2.4.11 version to Ol disassembly and finds these NIDs in OptOl and Ol under logical library libSceFreeType; it does not run real-face paths or establish all firmware versions. This implementation exports them only from OptOl; Ol remains missing. Sony prototypes, guest layouts, module/library versions and nonnull-face equivalence remain unverified. Signed positions, scales and vectors use 64-bit guest storage even on Windows. Invalid PFR advance indices write zero as the bundled service does; non-PFR advance errors leave output untouched, while non-PFR metrics write fallback values before returning the format error. Those nonnull rules still follow 2.13.3 and have not been measured in the console modules. + +### Functional + +- [PFR faces](../../../core/libs/prx/libSceFreeTypeOptOl/Export.cpp) share the existing libSceFreeType ownership and lifetime rules. The optional module forwards to native helpers in that library rather than constructing another FreeType runtime. Private face records, stale or arbitrary pointers, caller-replaced internal records and simultaneous operations on the same face are not made safe by these adapters; callers must obey FreeType's face lifetime and threading requirements. The synthetic PFR regression exercises metrics, signed advances, metric-space kerning and an empty outline; it does not test bitmap-only PFR faces or every format feature. diff --git a/docs/dev/debt/libSceFreeTypeOt.md b/docs/dev/debt/libSceFreeTypeOt.md new file mode 100644 index 0000000000..6e93bdf35f --- /dev/null +++ b/docs/dev/debt/libSceFreeTypeOt.md @@ -0,0 +1,9 @@ +# libSceFreeTypeOt technical debt + +### Unknown function info + +- [FT_Outline_GetInsideBorder, FT_Outline_GetOutsideBorder](../../../core/libs/prx/libSceFreeTypeOt/CMakeLists.txt) (libSceFreeTypeOl, libSceFreeTypeOt) - console prototypes, enum/layout equivalence, FreeType configuration and module/library versions are unverified. The existing LP64 outline layout and unsigned 16-bit counts follow bundled FreeType 2.13.3; border selection follows [ftstroke.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c), with the shifted polygon area from [ftoutln.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) accumulated in 64 bits. Null positive-count arrays and out-of-range or non-increasing contour endpoints throw when reached; their console behavior is unknown. Tags and flags are not read, and input arrays remain caller-owned. + +- The 140 `sceFreeTypeOtUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. + +- The FT_Outline_Get_Orientation, FT_Request_Size, FT_Sin, FT_Tan, FT_Vector_Unit, FT_New_Size, FT_Outline_Copy and FT_Outline_Decompose NIDs in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are identified, but their provider-specific prototypes and behavior remain unverified. Their named helpers retain the explicit inventory aliases and throw; the separate libSceFreeType implementations do not supply this provider. diff --git a/docs/dev/debt/libSceFsInternalForVsh.md b/docs/dev/debt/libSceFsInternalForVsh.md index 92591afa7b..4e242ae883 100644 --- a/docs/dev/debt/libSceFsInternalForVsh.md +++ b/docs/dev/debt/libSceFsInternalForVsh.md @@ -2,4 +2,4 @@ ### 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, sceFsErriCtl, sceFsErriGetState, 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 +- [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 diff --git a/docs/dev/debt/libSceGameLiveStreaming.md b/docs/dev/debt/libSceGameLiveStreaming.md index b52bed6644..ea70d1d1fc 100644 --- a/docs/dev/debt/libSceGameLiveStreaming.md +++ b/docs/dev/debt/libSceGameLiveStreaming.md @@ -5,7 +5,7 @@ - [sceGameLiveStreamingEnableLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) - a call outside `sceGameLiveStreamingInitialize` and `sceGameLiveStreamingTerminate` throws, as the module's error for it is not confirmed (`sceGameLiveStreamingTerminate` and `sceGameLiveStreamingGetCurrentStatus2` return `0x80A00004`; [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/src/ps4_libscegamelivestreaming.pas#L18) does not return its `SCE_GAME_LIVE_STREAMING_ERROR_NOT_INITIALIZED` from this call). Otherwise both values are accepted and change nothing, as no broadcast is ever on air. The signature, `int sceGameLiveStreamingEnableLiveStreaming(const bool)`, follows [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L810) and [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/src/ps4_libscegamelivestreaming.pas#L111) - [libSceGameLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) - native initialization requires exactly 0x4000 heap bytes before checking repeated initialization. Host lifecycle and pointer errors now match the wrapper, but service registration, allocation failures and asynchronous service readiness are not modelled. The host becomes ready immediately after initialization. - [sceGameLiveStreamingGetCurrentStatus2](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) - the supplied module confirms the 72-byte layout and inactive status (user -1, remaining bytes zero). Live service counters, active-user changes and broadcast-resolution conversion are unimplemented. -- [sceGameLiveStreamingGetProgramInfo](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - the PS5 12.00 module clears the 0x134C-byte program info, asks the system service for its first 0xFD4 bytes and returns `0x80A00002` for a null pointer and `0x80A00004` before initialization. With nothing broadcast here the info stays zero, which assumes the service reports an empty program when nothing is on air; the field layout is unknown +- [sceGameLiveStreamingGetProgramInfo](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - the PS5 13.60 module clears the 0x134C-byte program info, asks the system service for its first 0xFD4 bytes and returns `0x80A00002` for a null pointer and `0x80A00004` before initialization. With nothing broadcast here the info stays zero, which assumes the service reports an empty program when nothing is on air; the field layout is unknown. Two titles (PPSA03001 at 0x1ad86b4, PPSA07426 at 0xe89594) disassembled for this entry pass the address of a stack buffer with no store into it between the start of the block and the call; for a result other than 0 they do not read it, for 0 they copy three text fields at offsets 0, 0x802 and 0xC03, and they do not tell `0x80A00002` from `0x80A00004`. The buffer size is not shown. ### Unknown function info diff --git a/docs/dev/debt/libSceGameUpdate.md b/docs/dev/debt/libSceGameUpdate.md index fd8f0f341a..75626c8e75 100644 --- a/docs/dev/debt/libSceGameUpdate.md +++ b/docs/dev/debt/libSceGameUpdate.md @@ -1,7 +1,11 @@ # libSceGameUpdate technical debt +### Silent stubs + +- [sceGameUpdateCreateRequest, sceGameUpdateCheck](../../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - requests are kept in the module's 8 slots with ids from `0x20000000` (a ninth returns `0x80412806`, an aborted request's Check `0x80412807`), as [prosper](https://github.com/mattias800/prosper) reads the PS5 3.20 `libSceGameUpdate.sprx`. Check never asks a patch service: it always answers no update (`found` and the byte after it cleared, the rest of the result untouched, return 0), which is what the module returns when its service reports `0x80412883`; KytyPS5 also answers no update. What the service returns on an offline console is not measured. The answer only decides whether a title offers a patch. PPSA22408 aborted on the throw in sceGameUpdateCreateRequest at its title screen. Closes when: measured on a PS5 + ### Functional -- [libSceGameUpdate](../../../core/libs/prx/libSceGameUpdate/Export.cpp): initialization tracks host state but does not connect the patch service. Request creation and entitlement version lookup throw instead of fabricating an offline error or a successful no-update result. The original maintains eight service-backed requests with IDs starting at `0x20000000`; the service's offline response is still unknown. Check, Abort and Delete implement argument/state checks but have no live requests to operate on. +- [libSceGameUpdate](../../../core/libs/prx/libSceGameUpdate/Export.cpp): initialization tracks host state but does not connect the patch service. Entitlement version lookup throws. - [sceGameUpdateCheck](../../../core/libs/prx/libSceGameUpdate/Export.cpp): option zero is supported for argument validation. The original additionally permits option one when a kernel platform predicate succeeds; that platform mode is not modeled. - [sceGameUpdateGetAddcontLatestVersion](../../../core/libs/prx/libSceGameUpdate/Export.cpp): the supplied module confirms a 20-byte entitlement-label input and a 48-byte service output. Null pointers are rejected before initialization is checked. There is no local output-size validation or blanket clearing of the output; the host leaves it untouched when the missing service causes an exception. diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index 25f1bc36a6..e8cbc5eacd 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -6,7 +6,7 @@ ### Unknown function info -- [sceHmd2Open](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the handle is the value the console's driver returned to the first open; titles built with an SDK before 7.00 may open for any user, and the firmware's `sceMbusAddHandleByUserId` result for a user other than the initial one is unknown, so that case throws. 0x8a720006 for a second open is named `AlreadyOpened` from its use +- [sceHmd2Open](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the handle is the value the console's driver returned to the first open; titles built with an SDK before 7.00 may open for any user, and the firmware's `sceMbusAddHandleByUserId` result for a user other than the initial one is unknown, so that case throws. 0x8a720006 for a second open is named `AlreadyOpened` from its use. One title (PPSA29866) disassembled for this entry calls it once (0x2d1e2e) with a user ID from a variable of the title whose origin was not read, type 0, index 0 and a null parameter, stores the result in a field of its own object and tests it for a negative value. Which user it opens for is not shown. - [sceHmd2SetVibration](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the second parameter is checked against 25 only while a headset is connected; its name and unit are unknown. The field of view member names are assumed: of the firmware's default tangents the first is the widest and the last two are equal - [libSceHmd2 legacy exports](../../../core/libs/prx/libSceHmd2/Export.cpp) - SeeThroughDraw, SeeThroughIC, VideoOutEq, WaitShader, SeeThroughCallbackGetPoseDataFunc, ReprojectionClear, ReprojectionSet and RpeorjectionSetParam have no matching symbols or callers in the supplied system modules. Their signatures remain unresolved. ClearUserEventEnd/Start, GazeStart/StopUserCalibration and GazeObjectSelectionTerminate are confirmed to take no arguments; their behavior after initialization remains unimplemented. - [sceHmd2InternalSetPortStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature @@ -31,7 +31,7 @@ ### Functional -- [sceHmd2Initialize](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the firmware also fails with 0x8a72001f when `sceKernelGetAppInfo` fails and with 0x8a72000a for some applications, depending on app info fields at 0x2c, 0x4b and 0x4d whose meaning is unknown, and then calls `sceMbusInit`; none of this is modelled +- [sceHmd2Initialize](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the firmware also fails with 0x8a72001f when `sceKernelGetAppInfo` fails and with 0x8a72000a for some applications, depending on app info fields at 0x2c, 0x4b and 0x4d whose meaning is unknown, and then calls `sceMbusInit`; none of this is modelled. One title (PPSA29866) disassembled for this entry calls it once (0x2d2f6c) with the address of a zeroed 16-byte local block and hands the result straight to another function; a branch on the result was not read. - [Reprojection size/alignment queries](../../../core/libs/prx/libSceHmd2/Export.cpp) - all five exports consume no arguments and return two uint64 values in RAX:RDX. Four host queries return values measured without a headset; native display sizes are calculated through the graphics layout service and see-through size follows internal configuration. QueryBufferSizeAlign changes calculation with runtime state; its native pair return type is restored, while its protected body remains unchanged and throws. - [Hmd2BeforeInitializeZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers 0 to these 45 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `vDgpdS2N9Rw` (sceHmd2InternalRegisterVibrationCallback), `Ri35gKTZpvE` (sceHmd2InternalUnregisterVibrationCallback), `9oyW8puiKeI`, `1HZNE6jhBnQ` (sceHmd2InternalPowerEventInitialize), `cdwAKBZLXjs`, `Yub1MF58jfY`, `JYXAuQFnPyw` (sceHmd2InternalDfuOpen), `VuPKqCu3wzw` (sceHmd2InternalDfuClose), `GSThagVyW5U` (sceHmd2InternalCrashReportOpen), `J4qE5N937MY` (sceHmd2InternalCrashReportClose), `9KnNj-dyASg`, `Qta2bkstbFk`, `5ZcObnDRBXE`, `AvPNn0qx4WQ`, `jp0apqNGgi8` (sceHmd2SetIpdOffset), `-kZT7X7OMg0` (sceHmd2InternalRelocalizerOpen), `1ewLtng-SVk` (sceHmd2InternalRelocalizerClose), `B0b8DeIUGes` (sceHmd2InternalTrackingOpen), `HclLg4Oj5yw` (sceHmd2InternalTrackingClose), `VbEvrQXsZyY` (sceHmd2InternalLedDetectorOpen), `bB6MKZi5UCQ` (sceHmd2InternalLedDetectorClose), `OXQ1EDf02XU` (sceHmd2InternalImageOpen), `IAxfMTvFm0M` (sceHmd2InternalImageClose), `qTJtAtaLeno` (sceHmd2InternalGenDataOpen), `j7Q16UX6l34` (sceHmd2InternalGenDataClose), `pmmNM06rLqU`, `zdQfyDK9Ods` (sceHmd2InternalSlamTraceOpen), `aPRwp9GnljM` (sceHmd2InternalSlamTraceClose), `MjY1gBSaed0` (sceHmd2InternalGazeOpen), `evAMjJYHqW4` (sceHmd2InternalGazeClose), `r93FLEZ3BBk` (sceHmd2InternalGazeStart), `IdYURdEdNFo` (sceHmd2InternalGazeStop), `CVcL2Cu2AEI` (sceHmd2GazeActivateGazeTracking), `NmKIfVxr2s0`, `KeDLkkp62Gg`, `+aadu6-ra+A`, `FgnTiwwGSzk`, `PWAUwOTHbbo` (sceHmd2InternalCreateSharedMemory), `WzaEApHnsP8` (sceHmd2InternalIsGameVr2d), `dXzQI8QVY7Y` (sceHmd2InternalIsMiniAppVr2d), `cCvFKGqRh1k` (sceHmd2InternalIsCommonDlgVr2d), `uj4-2VEAFPU` (sceHmd2InternalIsCommonDlgMiniAppVr2d), `sIaGStI-iLM` (sceHmd2InternalMapSharedMemory), `JzwpZOemDO8`, `IsC4R4bZFVU`. 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 - [Hmd2BeforeInitializeNotInitialized](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720002` to these 9 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `sXaPEPY-fLA` (sceHmd2InternalOpen), `3F5inHhKzXg` (sceHmd2InternalGetDeviceInformation), `sJAiB-6p55U`, `E8hDydLjsSs`, `xEQkSb2WJ7Y`, `aaP4dnR4q-0`, `lEU0TeUoGwI`, `cFVyjHBpUUU`, `zROtb46jTj4`. 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/libSceHttp.md b/docs/dev/debt/libSceHttp.md index e58f4e6694..fdf9aeac06 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -30,6 +30,7 @@ - [sceHttpDbgShowRequestStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the native diagnostic consumes no arguments; it prints library state. The throwing implementation remains unchanged. - [sceHttpDbgShowMemoryPoolStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the native diagnostic consumes no arguments; it prints library state. The throwing implementation remains unchanged. - [sceHttpGetMemoryPoolStats](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - not measured on a console. The library takes nothing from the pool given to `sceHttpInit`, so it reports that size with peak and current use 0, where a console already holds bytes for a live context (0x1640 for Http2, see libSceNet). No live context returns `BEFORE_INIT` (0x80431001) and an unknown or terminated id `INVALID_ID` (0x80431100), as in `sceHttpCookieFlush`, with the output untouched; both codes are unmeasured. A null output throws, as its error code is unknown. Only the 24 bytes of the declared layout are written; [SharpProspero's struct](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs#L398-L413) adds a 32-bit reserved field after them +- [sceHttpGetRegisteredCtxIds](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - not measured on a console. It reports the libnet memory pool id and the SSL context id given to `sceHttpInit` for the context, in the order of the recovered prototype `(int contextId, int* netPoolId, int* sslContextId)`; no public source names its outputs, and neither id is checked against libSceNet or libSceSsl. No live context returns `BEFORE_INIT` and an unknown or terminated id `INVALID_ID`, as in `sceHttpGetMemoryPoolStats`, with the outputs untouched; both codes are unmeasured. A null output throws, as its error code is unknown ### Functional diff --git a/docs/dev/debt/libSceHttp2.md b/docs/dev/debt/libSceHttp2.md index 73186b8a90..b9e164b243 100644 --- a/docs/dev/debt/libSceHttp2.md +++ b/docs/dev/debt/libSceHttp2.md @@ -1,19 +1,21 @@ # libSceHttp2 technical debt -### Silent stubs - -- [libSceHttp2](../../../core/libs/prx/libSceHttp2/Export.cpp) - no request reaches the network, so `sceHttp2SetMinSslVersion` has no TLS connection to restrict and its value is ignored, as for `sceHttp2SslEnableOption` and `sceHttp2SslDisableOption`. The three setters validate `id` against the live handle registry and return `SCE_HTTP2_ERROR_INVALID_ID` (`0x817B1100`) for an invalid handle, following [prosper](https://github.com/mattias800/prosper). Whether the console restricts the object kind these setters accept, and what it does with an id from a previous init/term cycle, is unverified, so any live handle kind is accepted and ids are never reused. `sceHttp2Term`, `sceHttp2DeleteTemplate`, `sceHttp2DeleteRequest`, `sceHttp2CreateTemplate`, `sceHttp2CreateRequestWithURL` and `sceHttp2SendRequestAsync` also return the invalid-id error for a handle of the wrong kind or no longer live, following prosper (not measured on a console) - ### Unknown function info - [sceHttp2DeleteCookieBox](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - imported by PPSA35943's Unity player. The `(int cookieBoxId)` signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http2.cs#L215-L217). A box `sceHttp2CreateCookieBox` returned is discarded; an unknown or already deleted id returns the library's invalid-id error (0x817B1100), as [prosper](https://github.com/mattias800/prosper/blob/b1bec57186a0/prosper/src/hle/net/hle_http2.cpp#L512-L523) does; not measured on a console -- [sceHttp2SetResolveRetry](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - signature `(int id, int32_t retry)` from libSceHttp's `sceHttpSetResolveRetry`; the value is accepted and ignored like the other Http2 settings, since no request reaches the network -- [libSceHttp2 cookie boxes](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - `sceHttp2CreateCookieBox` takes a machine-sized pool size, requires at least 64 KiB and allocates independently of HTTP contexts. The host reserves the pool with 16 KiB rounding but does not reproduce native cookie-heap bookkeeping, identifier ranges or retained request references. Its delete signature and invalid-ID error are confirmed. `sceHttp2SetRequestNoContentLength` takes only the request id. Cookies are never stored, so `sceHttp2CookieFlush` has nothing to discard +- [libSceHttp2 cookie boxes](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - `sceHttp2CreateCookieBox` takes a machine-sized pool size, requires at least 64 KiB and allocates independently of HTTP contexts. The host reserves the pool with 16 KiB rounding but does not reproduce native cookie-heap bookkeeping, identifier ranges or retained request references. Its delete signature and invalid-ID error are confirmed. `sceHttp2SetRequestNoContentLength` takes only the request id. Cookie storage, nonzero cookie-box binding and `sceHttp2CookieFlush` throw - [libSceHttp2 pending exports](../../../core/libs/prx/libSceHttp2/Export.cpp) - 18 named callable exports were identified in the PS5 3.20 export table and cross-checked against firmware 13.60. Signatures and behavior remain unverified; declarations use the unknown-signature convention and throw through `NotImplemented_nid_no_patch`. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHttp2.c). ### Functional -- [libSceHttp2 asynchronous requests](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. PPSA15508 passes null kqueue and extra options at original VA 0xb63931, then calls `sceHttp2WaitAsync` with a 24-byte result and a 2,000,000 timeout, and checks the result at offset 8. A null kqueue option therefore retains the same offline completion for direct waiting without an event notification. The timeout is not used because the offline result is already complete. This path follows the original title call and caller control flow, not a console measurement. A supplied kqueue with a missing user event, an extra option, and a wait with no completed operation still throw +- [libSceHttp2 transport](../../../core/libs/prx/libSceHttp2/Transport.cpp) - only synchronous HTTPS requests with version 3 and ALPN `h2` are supported. HTTP/1.x, cleartext HTTP/2, negotiation fallback, connection reuse, multiplexing across requests, CONNECT, response trailers, automatic redirects and authentication challenges throw. Version values, method strings, header overwrite/append modes, status/read/content-length signatures and timeout units follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http2.cs); wire framing and negotiation follow [RFC 9113 sections 3, 5, 6 and 8](https://www.rfc-editor.org/rfc/rfc9113.html). [prosper](https://github.com/mattias800/prosper/blob/b1bec57186a0/prosper/src/hle/net/hle_http2.cpp) instead records the method argument as an integer; the string signature follows SharpProspero. These are public compatibility contracts, not console measurements. +- [libSceHttp2 error paths](../../../core/libs/prx/libSceHttp2/Transport.cpp) - cancellation, deadlines, DNS/TLS/protocol failures, malformed inputs, concurrent calls on one request and exhausted connection slots throw because their console error mappings are unknown. Socket connect errors propagate the libSceNet result, following the module-analysis report in [prosper #2894](https://github.com/mattias800/prosper/issues/2894); the report does not establish every exported error path. A request uses one connection; a context's concurrency cap counts connections until EOF, failure or deletion. Native pooling, scheduling, validation order and teardown semantics remain unverified. DNS uses two host workers and a queue of eight; cancellation/deadlines abandon the caller's wait, not libc's lookup. Library unload can wait for a host lookup to return. +- [libSceHttp2 TLS settings](../../../core/libs/prx/libSceHttp2/Export.cpp) - minimum-version selection, nonnull SSL callbacks and flags outside `0x3d` throw. Supported certificate-check bits follow SharpProspero and use libSceSsl's TLS 1.2 backend and CA context; native defaults and error translation are unverified. Settings on object kinds other than templates/requests throw; native object-kind restrictions are unverified. Settings become immutable when transport is created. Zero timeout values, nonzero resolve retries, gzip inflation and connection-slot wait timeouts throw. Host HTTP allocations are not charged to the context pool; `sceHttp2GetMemoryPoolStats` throws for live contexts instead of reporting zero usage. +- [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. - [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. + +- [libSceHttp2 automatic proxy configuration](../../../core/libs/prx/libSceHttp2/Export.cpp) - flags 0 and 1 both use direct connections, matching [libSceNetCtl](../../../core/libs/prx/libSceNetCtl/Export.cpp), which reports proxy configuration off, an empty server and port zero. Other flag values throw. This is the host configuration policy, not a console measurement of proxy discovery; PAC/WPAD and configured proxies remain unsupported. The boolean argument and acceptance of 1 by callers are evidenced by [OliSe Player](https://github.com/Marice/OliSePlayer/blob/fb06d7ac7467a50ba1f2ee299dac20e43213d674/src/netxm.c#L73) and [RetroArch](https://github.com/mihawk-99/PS5_RetroArch/blob/7268510b12fe7461342e0cc4497c87a0910c1055/src/webui_update.cpp#L305); call sites do not establish library behavior. +- [libSceHttp2 default trust](../../../core/libs/prx/libSceHttp2/Transport.cpp) - HTTPS currently uses only authorities explicitly loaded into the caller's libSceSsl context; a fresh context cannot validate public servers. Verification remains enabled. Loading host roots is left to the shared per-connection SSL work in [#11134](https://github.com/boykopovar/AnyPS5/pull/11134) and the stacked HTTP2 follow-up [#11145](https://github.com/boykopovar/AnyPS5/pull/11145), rather than adding a second trust-store implementation here. The exact console root set, trust updates and pool accounting remain unverified. This transport alone does not enable stock HTTPS callers that load no roots. diff --git a/docs/dev/debt/libSceHttpCache.md b/docs/dev/debt/libSceHttpCache.md index a2d7b748a6..b49e7d07ec 100644 --- a/docs/dev/debt/libSceHttpCache.md +++ b/docs/dev/debt/libSceHttpCache.md @@ -2,5 +2,11 @@ ### Unknown function info -- [sceHttpCacheSystemSendStatistics and sceHttpCacheSystemTerm](../../../core/libs/prx/libSceHttpCache/Export.cpp): the supplied implementations return zero without reading arguments. No callers have yet established whether their public prototypes include unused arguments. -- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceHttpCacheClearAllUserTags`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheClearCacheInfo`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheDebugSetUrl`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheRegisterCacheInfoUrl`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheRemoveTag`](../../../core/libs/prx/libSceHttpCache/Export.cpp). +- [sceHttpCacheDebugSetUrl, sceHttpCacheSystemSendStatistics and sceHttpCacheSystemTerm](../../../core/libs/prx/libSceHttpCache/Export.cpp): the supplied implementations return zero without reading arguments. No callers have yet established whether their public prototypes include unused arguments. +- [`sceHttpCacheClearCacheInfo` and `sceHttpCacheRemoveTag`](../../../core/libs/prx/libSceHttpCache/Export.cpp): signatures unverified; parameterless declarations that throw through `NotImplemented_nid_no_patch`. The pointer types of [`sceHttpCacheClearAllUserTags`](../../../core/libs/prx/libSceHttpCache/Export.cpp) (a list of at most 20 entries, taken as user ids) and [`sceHttpCacheRegisterCacheInfoUrl`](../../../core/libs/prx/libSceHttpCache/Export.cpp) are inferred from the checks. + +### Functional + +- [libSceHttpCache](../../../core/libs/prx/libSceHttpCache/Export.cpp) - cached responses live in process memory, keyed by user, URL and `User-Agent` value, instead of the console's per-user files, so they are lost when the process exits; they survive `Term`. Request states, freshness, revalidation and header rules follow firmware 13.60, read from disassembly, not measured on a console. A `public` response stored while cache sharing is on, which the console files under the store shared by all users, throws through `NotImplemented_nid_no_patch`, as do a lookup and a `ClearAll` by another user while such an entry exists, since whether they reach the shared store was not read. The console's debug request header that adds cache state to responses is not reproduced. Header numbers the firmware's `stoul` or `stol` rejects, a null URL in `Retrieve` (the firmware dereferences it) and an empty header list to return throw. +- [libSceHttpCache](../../../core/libs/prx/libSceHttpCache/Export.cpp) - `SetQuota`, `ClearAllUserTags` and a non-null `RegisterCacheInfoUrl` throw through `NotImplemented_nid_no_patch` once their arguments pass; `Clear`, `CreateRequestWithTag`, `RemoveTag`, `ClearCacheInfo`, both `WithMemoryPool` forms, `SystemInit`, `SystemClearAll` and `SystemShutdown` throw outright, so no entry has a tag or a quota. `Init` ignores its pool size, so the pool allocation failure is not reproduced, and storing never fails, so the console's file errors are not reproduced. +- [libSceHttpCache](../../../core/libs/prx/libSceHttpCache/Export.cpp) - the freshness lifetime is `Expires` minus `Date`, or a tenth of `Date` minus `Last-Modified`, computed on unsigned ticks without comparing them first, and an entry is fresh while its age is below that lifetime, as in firmware 13.60, read from disassembly, not measured on a console. An `Expires` earlier than `Date`, or a `Last-Modified` later than `Date`, therefore wraps to a lifetime that never runs out, so the entry keeps being returned as a hit instead of going stale as RFC 9111 asks; this is kept to match the console. diff --git a/docs/dev/debt/libSceIcu.md b/docs/dev/debt/libSceIcu.md new file mode 100644 index 0000000000..dcf66e6027 --- /dev/null +++ b/docs/dev/debt/libSceIcu.md @@ -0,0 +1,5 @@ +# libSceIcu technical debt + +### Functional + +- [Invariant character and version helpers](../../../core/libs/prx/libSceIcu/Version.cpp) are unverified on a console. `u_charsToUChars`, `u_UCharsToChars`, `u_versionFromString`, `u_versionFromUString` and `u_versionToString` follow ICU 78.1 [uinvchar.cpp](https://github.com/unicode-org/icu/blob/release-78.1/icu4c/source/common/uinvchar.cpp) and [putil.cpp](https://github.com/unicode-org/icu/blob/release-78.1/icu4c/source/common/putil.cpp), unchanged since 65.1. Firmware 13.42 reports ICU 78.1 and exports none of them ([#11013](https://github.com/boykopovar/AnyPS5/pull/11013#issuecomment-6400470698)); the [3.20 table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIcu.c) has all five. Version parts use the guest `strtoul`, truncated to eight bits; `u_versionToString` keeps ICU's dropped zero tens digit (105 becomes `15`). A null buffer with a positive conversion length throws; ICU dereferences it. Closes when: compared with the console. diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index b26437726a..8902fc8071 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -11,6 +11,7 @@ - [sceImeGetPanelPositionAndForm](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills. While a panel is open it reports the type, position and alignments passed to `sceImeOpen` and the size `sceImeGetPanelSize` gives; the console's values for a panel moved by its own position adjustment are unknown. - [sceImeFilterText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the behaviour follows the disassembly of `libSceIme.sprx` from firmware 13.60 and is differential-tested against that function in an emulator, not measured on a console. [ImeFilterCodepages.hpp](../../../core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp) holds the two codepage checks (helpers at 0x4850 and 0x48f0), rebuilt from Unicode's public [CP932](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT) and [CP936](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT) tables plus the Latin-1, CJK Extension A and a few other units the firmware also accepts (listed in the PR). The parameter layout beyond `type` is inferred: `mask` is the allowed-class bitmask, and `version` and `variant` derive it when it is 0. [sceImeCheckFilterText](../../../core/libs/prx/libSceIme/Export.cpp) throws, as its contract is unknown - [sceImeOpen, sceImeUpdate, sceImeSetText, sceImeSetCaret, sceImeClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the event ids (Open 0, UpdateText 1, UpdateCaret 2, PressClose 4, PressEnter 5), the caret directions (left 1, right 2), the error codes and the text event payload follow the PS4 reimplementation in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b72489d8c24e63fc56a29fe2c90120ab768a210d/src/core/libraries/ime/ime.cpp); none is measured on a PS5. Events are queued and delivered by `sceImeUpdate` only when it is passed the handler given to `sceImeOpen`, with that call's `arg`. A text event points `str` at a copy of the text kept in `work`, and `inputTextBuffer` holds the same text; both are terminated, `caret_index` is the caret after the edit and the one edit area has the edit position and the change in UTF-16 units (0 for Enter and Close). An edit sends one UpdateText; a caret move sends one UpdateCaret per UTF-16 unit, as shadPS4 does. Enter inserts a line break with the multiline option and sends PressEnter otherwise; Escape sends PressClose. Typed text is cut to `maxTextLength` without splitting a surrogate pair, line breaks are dropped without the multiline option, and the BasicLatin, URL, mail and number types do not filter characters. `sceImeOpen` checks, in shadPS4's order, the user, type, enter label, input method, option bits (the `sceImeGetPanelSize` mask), `maxTextLength` (1 to 2048), the text buffer, the position (below 1920x1080, or 3840x2160 with the over-2K option), the alignments, the extended priority and reserved bytes, `work` (non-null, 4-byte aligned), the handler, the reserved bytes and the initial text (no unpaired surrogate, no line break without the multiline option); shadPS4's SDK-dependent option, language and extended-option masks and its work-overlap check are not applied, and only the text copy is written to `work`. `sceImeSetText` keeps the caret unless the text became shorter, `sceImeSetCaret` rejects an index past the text with `0x80bc0030`, and neither sends an event. `sceImeSetTextGeometry` accepts any geometry while a panel is open, since no panel is drawn +- [sceImeUpdate and sceImeKeyboardOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - returning 0x80BC0031 (ErrorInvalidAddress) for a null parameter in `sceImeKeyboardOpen` follows the reimplementations in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b72489d8c24e63fc56a29fe2c90120ab768a210d/src/core/libraries/ime/ime.cpp#L557-L560) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/1211958b4eb38256cc0c5c7ba92d668820c05389/src/libs/ime.cpp#L540-L542). In contrast, returning 0x80BC0022 (ErrorInvalidHandler) from `sceImeUpdate(nullptr)` is an assumption based on the error code name and sibling parameter checks; neither shadPS4 nor KytyPS5 returns INVALID_HANDLER for a null handler (both return NOT_OPENED when no session or keyboard is open or when no active handler matches), so the existing shadPS4 reference does not establish this result. The regression tests establish the host implementation's behavior rather than verified native return codes. - [sceImeForTestFunction](../../../core/libs/prx/libSceIme/Export.cpp) returns a constant error without consuming arguments in the supplied module; unused argument types cannot be recovered from that body. - [sceImeConfigSet and sceImeConfigGet](../../../core/libs/prx/libSceIme/Export.cpp) preserve the native argument slots: an unused first slot, a 64-bit configuration value, a 32-bit key and, for set, a 32-bit value. The service receives a 24-byte request; configuration/key semantics and unused argument types remain unresolved. The getter advertises one output transfer but does not initialize its descriptor in this module. Protected bodies remain unchanged. @@ -18,7 +19,7 @@ ### Functional -- [sceImeKeyboardOpen, sceImeKeyboardClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - opening a keyboard that is already open for the same user returns `0x80bc0001` (busy) and closing one that is not open returns `0x80bc0002`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/1211958b4eb38256cc0c5c7ba92d668820c05389/src/libs/ime.cpp) does; it tracks one keyboard for all users, while the open state is kept per user id here, and the PS5 result is unverified +- [sceImeKeyboardOpen, sceImeKeyboardClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - opening a keyboard that is already open for the same user returns `0x80bc0001` (busy) and closing one that is not open returns `0x80bc0002`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/1211958b4eb38256cc0c5c7ba92d668820c05389/src/libs/ime.cpp) does; it tracks one keyboard for all users, while the open state is kept per user id here, and the PS5 result is unverified. Five titles disassembled for this entry have 8 Open call sites (PPSA03001 0x2073f79, PPSA07426 0x14682d9, PPSA29866 0x2d8fa8, PPSA30601 0x15e4934, PPSA04354 0xeac8b9, 0xeac8f6, 0xeac933 and 0xeac970) and 6 Close call sites, none reading the result. Open's result is tested only for 0 or ignored, except in PPSA29866: 0x80bc0001 gives an error value of its own and 0x80bc0002 an assertion output. - [sceImeKeyboardGetInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) returns `0x80bc0023` (no resource id) for every id while a keyboard is open, since the IME keyboard never reports a connected device. [sceImeKeyboardSetMode](../../../core/libs/prx/libSceIme/Export.cpp) validates the mode bits but the mode has no effect, as no IME keyboard events are delivered. - [sceImeKeyboardGetResourceId](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) reports that no USB keyboard is connected (`0x80bc0004`, as [shadPS4](https://github.com/shadps4-emu/shadPS4) does) because IME keyboard events are not delivered to the handler, although [libSceKeyboard](../../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) exposes the host keyboard - [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 diff --git a/docs/dev/debt/libSceImeBackend.md b/docs/dev/debt/libSceImeBackend.md new file mode 100644 index 0000000000..fc528a3311 --- /dev/null +++ b/docs/dev/debt/libSceImeBackend.md @@ -0,0 +1,5 @@ +# libSceImeBackend technical debt + +### Unknown function info + +- [sceImeBackendParamInit, sceImeBackendOpen, sceImeBackendClose, sceImeBackendReset, sceImeBackendGetStatus, sceImeBackendEnterCharacter, sceImeBackendBackspaceCharacter, sceImeBackendPartConfirm, sceImeBackendAllConfirm, sceImeBackendAllDeleteConvertString, sceImeBackendConvertForward, sceImeBackendConvertBackward, sceImeBackendConvertCancel, sceImeBackendGetString, sceImeBackendGetConvertString, sceImeBackendGetConfirmString, sceImeBackendGetCaretIndex, sceImeBackendModeCaretNext, sceImeBackendModeCaretPrevious, sceImeBackendGetCandidateList, sceImeBackendGetCandidateListSize, sceImeBackendGetCandidateSelect](../../../core/libs/prx/libSceImeBackend/Export.cpp) (libSceImeBackend) - declared without parameters: their signatures and behaviour are not established, so every call throws. PPSA05110 imports them and does not load without them. diff --git a/docs/dev/debt/libSceJobManager.md b/docs/dev/debt/libSceJobManager.md index 06f7ba4aac..93d8f3ec8a 100644 --- a/docs/dev/debt/libSceJobManager.md +++ b/docs/dev/debt/libSceJobManager.md @@ -2,8 +2,7 @@ ### Functional -- [sce::Job::JobManager](../../../core/libs/prx/libSceJobManager/Export.cpp) submits commands as work becomes runnable rather than encoding ahead with the rewind protocol recovered from PPSA06888's module (offsets 0x2210 and 0x2650). Commands are recycled after a completed wait. Command capacity is a separate bound from the dispatch count: the native size query measures one register and a 31-character marker, then its exhaustion callback at 0x29d0 traps instead of growing storage. The fallback throws. Original-module execution with the host AGC builders accepts two dispatches with 255 registers and 65,535-character markers, then traps on a third. Console timing equivalence remains unverified. -- The [manager](../../../core/libs/prx/libSceJobManager/Export.cpp) keeps scheduling state and nodes on the host heap; only the header, compute commands and completion labels occupy the caller's block. Blocks smaller than 0x2d8 bytes throw. Worker parking uses host mutexes and condition variables instead of native atomic queues and semaphores; console scheduling and spin timing are unverified. Spin intervals use unscaled host TSC, while the original module reads scaled guest TSC under the host-only `APS5_TIME_SCALE` diagnostic. -- [Sequence and manager operations](../../../core/libs/prx/libSceJobManager/Export.cpp) throw on a compute dispatch on a CPU sequence, a failing `sceAgcDriverAddEqEvent`, a factory priority above 1, and use of a destroyed sequence. `shutdown` throws while compute work is in flight or a caller is blocked in a sequence wait. Worker creation and attribute failures throw where the original does not check. -- [`initialize`](../../../core/libs/prx/libSceJobManager/Export.cpp) returns 0 instead of the result of the final object-registration import (`BG26hBGiNlw`, `_sceUlobjmgrRegisterObject`). The `sceRazorCpuJobManager*` profiler calls are not made. Sequence and factory virtual-function order is recovered from module vtables; no calling title has been run. -- The [fallback](../../../core/libs/prx/libSceJobManager/Export.cpp) differs from the owned module under host kernel services. Native Wait can stall allocating from an exhausted global node pool; the fallback deliberately needs no Wait node and recycles nodes. The owned module also stalls in the zero-spin, zero-persistence `worker_wait` fixture on Windows and Linux, while the fallback wakes a spare worker and completes the child. Console behavior for these cases is unverified. The [reference runner](../../../tools/run_job_manager_reference.py) on this branch supports Windows only; failures remain failures, and Linux runner support follows in #10272. +- [Compute sequences](../../../core/libs/prx/libSceJobManager/Export.cpp) encode ahead across GPU barriers using explicit release packets instead of the native rewind protocol and completion-packet patching recovered from PPSA06888's module (offsets 0x2210 and 0x2650). Commands are recycled after a completed wait. Command capacity is a separate bound from the dispatch count: the native size query measures one register and a 31-character marker, then its exhaustion callback at 0x29d0 traps instead of growing storage. The fallback throws. Original-module execution with the host AGC builders accepts two dispatches with 255 registers and 65,535-character markers, then traps on a third. Console timing equivalence remains unverified. Compute dispatch on a CPU sequence throws. +- [Scheduling](../../../core/libs/prx/libSceJobManager/Export.cpp) uses host mutexes and condition variables instead of native atomic queues and semaphores; spin intervals deliberately use unscaled host TSC, while the original module reads scaled guest TSC under the host-only `APS5_TIME_SCALE` diagnostic. Blocks below 0x2d8 bytes, factory priorities above one, use of destroyed sequences and shutdown from a worker throw. New calls concurrent with shutdown are unverified. +- [Pool allocation](../../../core/libs/prx/libSceJobManager/Export.cpp) reuses freed nodes in a different order; profiler dispatch IDs can differ from native allocation. The fallback Wait deliberately allocates no pool node; native Wait can stall on global pool exhaustion. Resource-creation failures throw after cleanup, where the original does not check every return. Vtable order was checked, but no calling title or PS5 timing equivalence has been tested. +- Console behavior for [worker waits](../../../core/libs/prx/libSceJobManager/Export.cpp) is unverified. The owned original module stalls on the zero-spin, zero-persistence `worker_wait` fixture with native Windows and Linux host kernel services; the fallback wakes a spare worker and completes the child. The reference runner keeps this case selectable but excludes it from the shared-behavior parity suite. diff --git a/docs/dev/debt/libSceJpegDec.md b/docs/dev/debt/libSceJpegDec.md index 174718fdd4..af33d224c8 100644 --- a/docs/dev/debt/libSceJpegDec.md +++ b/docs/dev/debt/libSceJpegDec.md @@ -7,4 +7,4 @@ ### Functional - [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp): work-memory size, alignment, creation checks, image-info fields, progressive coefficient-buffer capacity, row-buffer capacity and scaled dimensions are recovered from the native module. The host still allocates its decoder storage separately rather than using the guest work and coefficient regions. Malformed-header diagnostics and output mutation on failure are not fully matched. -- [sceJpegDecDecode](../../../core/libs/prx/libSceJpegDec/Export.cpp): decode mode 1 and pixel formats 9, 10 and 11 remain unimplemented and throw. Exact decoded pixel values and color conversion are not verified against the native decoder. +- [sceJpegDecDecode](../../../core/libs/prx/libSceJpegDec/Export.cpp): decode mode 1 and pixel formats 9, 10 and 11 remain unimplemented and throw. Exact decoded pixel values and color conversion are not verified against the native decoder. One title (PPSA29866) disassembled for this entry has one call site (0x2c111d); it passes a null `imageInfo` and a parameter structure with decode mode 0, down-scale 1 and pixel format 0, and hands the result to another function whose handling of a negative result was not read. In the parameter structure built around this call site, mode 1 and formats 9, 10 and 11 do not show up; whether a down-scale of 1 means no scaling was not compared. diff --git a/docs/dev/debt/libSceJxr.md b/docs/dev/debt/libSceJxr.md index 0fbc29eb6c..7e2a8ea81f 100644 --- a/docs/dev/debt/libSceJxr.md +++ b/docs/dev/debt/libSceJxr.md @@ -1,9 +1,9 @@ # libSceJxr technical debt -### Unknown function info - -- [libSceJxr pending exports](../../../core/libs/prx/libSceJxr/Export.cpp) - 4 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceJxr.c) and were cross-checked against the same module in PS5 firmware 13.60. `PixelFormatLookup`, `readIS` and `writeIS` have unverified signatures and behaviour; their declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - ### Functional +- [PixelFormatLookup](../../../core/libs/prx/libSceJxr/src/PixelFormatLookup.cpp) uses pinned public jxrlib through a C bridge and a 56-byte LP64 guest record. [BunnyFl reports firmware 13.42 native-PC comparisons](https://github.com/boykopovar/AnyPS5/pull/9954#issuecomment-6406356097): the 68 table entries match public jxrlib, and return values/record fields match across 802 cases repeated three times, except that the module clears padding bytes 28..31 on all 309 successes. This bridge now clears that four-byte range explicitly on success; it does not copy indeterminate C++ padding. This is attributed single-firmware evidence, not an author-run console measurement or proof of the complete Sony ABI/configuration. Module/library versions, other firmware and valid-mode null arguments remain unverified. Modes 0/1 perform forward GUID/reverse TIFF lookup; other modes and misses leave the entire record unchanged. Valid-mode null records and forward null GUIDs remain explicitly unsupported. Success borrows a read-only static GUID while the module is loaded; callers must not free it. Closes when: remaining Sony ABI, version/configuration and invalid-argument behavior are verified by attributable measurements or disassembly + - [PKFree](../../../core/libs/prx/libSceJxr/Export.cpp) (libSceJxr) follows Microsoft's jxrlib [JXRGlue.c](https://github.com/4creators/jxrlib/blob/f7521879862b9085318e814c6157490dd9dbbdb4/jxrgluelib/JXRGlue.c#L53-L62), whose four exported names the module shares: a non-null `ppv` has `*ppv` released with the guest `free` and set to null, and the result is always `WMP_errSuccess` (0) as a 64-bit `ERR` (`long`). That the console's module is built from jxrlib, and that its `PKFree` releases through libc's `free`, is inferred from the export names and not checked against the console's code. Closes when: the console's `PKFree` is disassembled or measured on a PS5 + +- [readIS and writeIS](../../../core/libs/prx/libSceJxr/Export.cpp) use the non-ARM, LP64 `BitIOInfo` and `WMPStream` ABI and packet algorithm from [Microsoft's JPEG XR reference implementation](https://github.com/4creators/jxrlib/blob/f7521879862b9085318e814c6157490dd9dbbdb4/image/sys/strcodec.c#L1090-L1177). Console structure layouts and execution remain unverified. Profiling callbacks are omitted as in the reference Unix build with `DISABLE_PERF_MEASUREMENT`; the codec argument is unused. Invalid null state or required callbacks throw instead of dereferencing them. No codec is instantiated by these helpers; callers must supply the reference ring buffer and guest-ABI stream callbacks. diff --git a/docs/dev/debt/libSceKeyboard.md b/docs/dev/debt/libSceKeyboard.md index ba3dcb499f..12c1ff0c79 100644 --- a/docs/dev/debt/libSceKeyboard.md +++ b/docs/dev/debt/libSceKeyboard.md @@ -2,7 +2,7 @@ ### Functional -- [sceKeyboardGetKey2Char](../../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp): the original validates HID usages 4 through 231 before the handle and preserves unconverted HID usages in the output with `processed == 0`. The host instead accepts out-of-range usages and writes zero for unconverted keys. The original ignores the Kana LED, while the host throws for it. This partially implemented function contains `NotImplemented_nid_no_patch(__func__)`; its body is preserved under the requested restriction. One title (PPSA15246) disassembled for this entry calls it once with an output buffer cleared to 24 bytes, uses a character only when the result is 0 and the first output byte is nonzero, and then reads the 16-bit value at offset 8, which agrees with `processed` and `char_code` as laid out here. Which other output fields it reads and what its other arguments mean are not shown. +- [sceKeyboardGetKey2Char](../../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp): initialization, output length and reserved bytes remain unverified on a console; unconverted keys retain the host's length of 0. One title (PPSA15246) disassembled for this entry calls it after clearing the first 20 bytes of its output buffer (16-byte store 0x69942d, 32-bit store 0x699435; no size argument), uses a character only when the result is 0 and the first output byte is nonzero, and then reads the 16-bit value at offset 8. Which other output fields it reads and what its other arguments mean are not shown. ### Unknown function info diff --git a/docs/dev/debt/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md index b1cb33da7a..3ac4b14e2d 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -13,7 +13,7 @@ - [Thread exit destructors](../../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped - [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 no process. FreeBSD 11.4 returns the error the child's exec leaves ([`_posix_spawn_thr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/posix_spawn.c#L227-L231), [`do_posix_spawn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/posix_spawn.c#L295-L303)), and `posix_spawnp` searches `PATH`, or `_PATH_DEFPATH` when it is unset, ending with `ENOENT` when no directory holds the program ([`_execvpe`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/exec.c#L272-L281), [`execvPe`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/exec.c#L257-L260)). So when the program is not in the guest file system both return `ENOENT` and leave the process ID unwritten; when it is there they throw. PPSA99305 (Dusklight) reaches this when SDL looks for `zenity` to show a file dialog. The attributes and file actions are not applied, so one that would fail in the child is not reported, and whether the console's application sandbox lets the child be created at all (FreeBSD returns that error first) is unverified. The attribute and file action calls keep FreeBSD 11.4's objects. Null or uninitialized objects, which FreeBSD dereferences, throw. +- [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` - [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 - [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 - [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 diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index f0d6d1c3bb..ac82d3fa4a 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,9 +2,23 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 357 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. -- [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); the module's LibreSSL version is unknown and nothing is checked on the console. Behaviour that differs between LibreSSL versions throws: `SHA256` with a null digest buffer (older versions return a static buffer) and `SHA512_Final` with the SHA-512/224 or SHA-512/256 digest length. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 321 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); nothing is checked on the console. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults - [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. +- [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) (libSceLibreSsl) - no export of this library adds an error yet, so the queue stays empty for guest code. The per-thread queue of 16 slots holding 15 codes, its order and the `ERR_PACK` layout follow LibreSSL's [err.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/err/err.c) and [err.h](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/err/err.h); the module's LibreSSL version is unknown and nothing is checked on the console. The queue keeps no file, line or data, as no export reads them. `ERR_remove_thread_state` with a thread id throws: that version asserts a null id, while older versions remove the state of the thread the id names +- [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_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 + +### Functional + +- [ASN1_INTEGER_get](../../../core/libs/prx/libSceLibreSsl/src/Asn1.cpp) has no console verification for nonpositive lengths. It follows [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1/a_int.c#L363-L389): valid integer types return zero without reading data when length is zero or negative; invalid types return -1 first. Later LibreSSL versions reject negative lengths through `ASN1_INTEGER_get_int64`; that later contract is not used for this library. + +- [ASN1_INTEGER_get and ASN1_STRING_data/length/type/to_UTF8](../../../core/libs/prx/libSceLibreSsl/src/Asn1.cpp) are unverified on PS5. Signatures, the 24-byte guest string layout and conversions follow [LibreSSL 2.5.4's ASN.1 sources](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1), the version reported for firmware 13.40 in [discussion #8981](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Null accessor inputs, null conversion outputs, missing nonempty data, oversized converted output and unrepresentable signed integer values throw where the source has undefined behaviour. Malformed encodings and allocation failure throw until ASN.1 error-queue reporting is implemented. Conversion buffers use the guest libc allocator and can be released with guest `free`. + +- [SSL_alert_desc_string_long](../../../core/libs/prx/libSceLibreSsl/Alert.cpp) - Console execution and equivalence across other firmware revisions remain unverified. BunnyFl reports identical strings for 3,841 inputs across three runs against the firmware 13.42 module executed natively on a PC ([comparison](https://github.com/boykopovar/AnyPS5/pull/9870#issuecomment-6406774281)); this is not a console run. The mapping follows LibreSSL 2.5.4. diff --git a/docs/dev/debt/libSceLibreSsl3.md b/docs/dev/debt/libSceLibreSsl3.md index dba672f91f..c98952a914 100644 --- a/docs/dev/debt/libSceLibreSsl3.md +++ b/docs/dev/debt/libSceLibreSsl3.md @@ -7,4 +7,4 @@ ### Functional - [CRYPTO_memcmp](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) follows the public [LibreSSL signature and equality contract](https://man.openbsd.org/CRYPTO_memcmp.3), without PS5 verification. The exact nonzero result for unequal buffers is unspecified by that contract. Content-independent execution of its volatile byte reads still depends on the compiler; inspect optimized code when changing toolchains. Closes when: measured on a PS5 and the supported toolchains' generated code is checked. -- [MD5 and SHA1](../../../core/libs/prx/libSceLibreSsl3/src/Digest.cpp) (libSceLibreSsl3) - follow LibreSSL's [md5.c, sha1.c](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto) and hash through libSceLibreSsl's `MD5_*` and `SHA1_*`. A null digest buffer throws: LibreSSL up to 3.8 returns a static buffer, later versions do not check it. Nothing is checked on the console +- [MD5 and SHA1](../../../core/libs/prx/libSceLibreSsl3/src/Digest.cpp) (libSceLibreSsl3) - follow LibreSSL's [md5.c, sha1.c](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto) and hash through libSceLibreSsl's `MD5_*` and `SHA1_*`. A null digest buffer, which LibreSSL 4.0.0 does not check, throws. Nothing is checked on the console diff --git a/docs/dev/debt/libSceMove.md b/docs/dev/debt/libSceMove.md index bc12dfcafb..3b9e712351 100644 --- a/docs/dev/debt/libSceMove.md +++ b/docs/dev/debt/libSceMove.md @@ -4,4 +4,4 @@ - [libSceMove](../../../core/libs/prx/libSceMove/Export.cpp): the host implements only disconnected controllers. The native module confirms initialization errors, disconnected return 1 without output writes, invalid-handle error 0x80EE0004, and initialization then argument then handle validation for queries. Reset-light-sphere still omits the device ioctl and its failure mapping. - [sceMoveOpen and sceMoveClose](../../../core/libs/prx/libSceMove/Export.cpp): protected bodies remain unchanged. Native opening resolves a user/device through the input service, rejects duplicate device handles with 0x80EE0007, limits storage to eight slots with 0x80EE0005, and obtains handles from the device ioctl. The host generates monotonically increasing handles, accepts arbitrary type/index values and returns 0x80EE0007 for duplicate slots and throws for other users. Closing an unknown handle returns the confirmed 0x80EE0004. -- [sceMoveReadStateRecent](../../../core/libs/prx/libSceMove/Export.cpp): the native module permits a null data array for SDK versions through 0x03ffffff while still requiring a count pointer and nonnegative timestamp. The host requires the data array for every SDK version. Connected state records and device-service failures remain unimplemented. +- [sceMoveReadStateRecent](../../../core/libs/prx/libSceMove/Export.cpp): a null data array is accepted for SDK versions through 0x03ffffff and rejected from 0x04000000 on, as the native module does; the count pointer and a nonnegative timestamp are required for every version. Connected state records and device-service failures remain unimplemented. diff --git a/docs/dev/debt/libSceMsgDialog.md b/docs/dev/debt/libSceMsgDialog.md new file mode 100644 index 0000000000..71acb51911 --- /dev/null +++ b/docs/dev/debt/libSceMsgDialog.md @@ -0,0 +1,9 @@ +# libSceMsgDialog technical debt + +### Silent stubs + +- [libSceMsgDialog](../../../core/libs/prx/libSceMsgDialog/Export.cpp) - message dialogs show no UI. `sceMsgDialogOpen` opens one without `sceMsgDialogInitialize` as well, and a user message or system message dialog finishes on the second `sceMsgDialogGetStatus` or `sceMsgDialogUpdateStatus` call; the first 160 characters of a user message are only logged. The button it picks (2 for `YES_NO_FOCUS_NO`, otherwise 1) is kept but not returned, as `sceMsgDialogGetResult` is exported by libSceMsgDialog.native. A progress bar dialog runs until `sceMsgDialogClose` or for 30 seconds, and `sceMsgDialogProgressBarInc`, `sceMsgDialogProgressBarSetMsg` and `sceMsgDialogProgressBarSetValue` ignore their arguments + +### Unknown function info + +- [sceMsgDialogOpen](../../../core/libs/prx/libSceMsgDialog/Export.cpp) (libSceMsgDialog) - the `SceMsgDialogParam` layout it reads is not measured on a PS5: the mode at 0x38 (1 user message, 2 progress bar, 3 system message), the user message parameter pointer at 0x40, and in that parameter the button type at 0 and the message pointer at 8. A mode outside 1 to 3, or a parameter that cannot be read, is treated as a user message; only Windows checks that the memory is readable, so on Linux any non-null pointer is read. The errors `0x80b80003` (not initialized), `0x80b80007` (busy), `0x80b8000b` (not running) and `0x80b8000d` (null argument) are not measured either. Closes when: measured on a PS5 diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 611efe6936..bdb60647aa 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -8,6 +8,7 @@ ### Unknown function info +- [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) - `SO_PROTOCOL` on raw sockets throws: FreeBSD's raw protocol-switch selection has not been mapped to host raw sockets. For stream and datagram sockets, the effective creation protocol and accepted-socket inheritance follow FreeBSD 11.4's [protocol tables](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/in_proto.c) and [sogetopt](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_socket.c); the `0x1016` option and truncated integer output are not measured on PS5. Existing null-pointer validation still returns `EINVAL`. - [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - zero-length TCP receives while connecting, after disconnect, or with a pending host error still use the host receive path. Error and output precedence in those states is unmeasured on a console. The established, error-free case follows FreeBSD 11.4's [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1568-L1583). - [sceNetConfigAddIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - disassembly confirms the same interface/address/broadcast/netmask arguments as SetIfaddr, entering its shared handler with add mode selected. GetIfaddr copies up to two 16-byte interface-address records. GetRouteInfo uses a family, record buffer, signed capacity and byte flags; internal statistics output is 16 bytes. The address-record and internal-statistics fields remain incomplete. @@ -17,7 +18,7 @@ - [sceHttp2GetMemoryPoolStats, sceSslGetMemoryPoolStats, sceNetGetMemoryPoolStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceHttp2, libSceSsl, libSceNet) - the signatures `(int id, stats*)`, the layouts (pool size, peak and current use, and for Http2 and Ssl a 32-bit reserved field) and the error codes were measured on a PS5 (firmware 13.60). The pool size is the one given at init or pool creation. Http2 and Net allocate nothing from these pools. Ssl counts its certificate backend allocations and returned data against its pool; C++ bookkeeping is outside that budget. All three initially report zero use, where the console already reports 0x15C0 bytes for Ssl, 0x1640 for Http2 and 4 for a Net pool. Ssl's allocation sizes and peak usage do not reproduce the console allocator - [sceNetClearDnsCache](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the library has no DNS cache to clear because lookups use the host resolver. The host resolver cache is left unchanged. - [sceNetEtherStrton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - strings shorter than 17 bytes return EINVAL without changing the output, rather than reading past the terminating NUL as in the [firmware 13.60 probe](https://gist.github.com/orezraey/789290577123af3c45b340d6db6f395c/71e0a6215c7ee86f49fdb31de989dd2b50720b7a). Non-ASCII digit bytes throw because their decoding is unmeasured; other firmware versions remain unverified. Fixed character pairs, ignored separators, alphabetic values beyond f, non-alphanumeric ASCII values of 0x7f, and null-pointer EINVAL follow those measurements, not the strict [shadPS4 parser](https://github.com/shadps4-emu/shadPS4/blob/d615b3887ba29025a59242e1a26f5b68bb176158/src/core/libraries/net/net.cpp#L1274-L1297) -- [sceNetGetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(info, int flags)` signature, the two IPv4 server slots in network byte order and the server count as the return value follow the PS4 [OpenOrbis musl resolver](https://github.com/OpenOrbis/musl/blob/2b0bb8ce2fa64ac255cb656785729d159a5f074c/src/network/resolvconf.c) and [OpenOrbis Net.h](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/Net.h); unverified on PS5 hardware. It reports the host DNS servers of the interface `sceNetCtlGetInfo` reports, never servers set through `sceNetSetDnsInfo`. Zeroing the unused slots and returning 0 without a host IPv4 interface are unverified. Nonzero flags and a null info throw because their console results are unknown +- [sceNetGetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - host-interface selection, zeroing unused slots and returning 0 without a host IPv4 interface remain unverified on PS5 hardware. It reports the host interface's DNS servers until a guest override is set. The two network-order IPv4 slots and returned count follow the PS4 [OpenOrbis musl resolver](https://github.com/OpenOrbis/musl/blob/2b0bb8ce2fa64ac255cb656785729d159a5f074c/src/network/resolvconf.c) and [Net.h](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/Net.h). - [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - generates values with the host random device; the system library maintains its own seeded PRNG state. - [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - for IPv6 a null `dst` returns `NET_EINVAL` here, where it crashed a payload process on a PS5 (firmware 13.60). The `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp). On PS4 firmware 10.71 and on that PS5, IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at a space, tab or VT), except that a part starting with `09` is decimal (`"0989"` is 989) and an octal part with `8` or `9` fails; flags were ignored (PS4: 0, single bits up to 0x100, 0x10000, 0x80000000, -1; PS5: 1, 2, 4, 0x100, 0x10000, -1). On the PS5 only: LF, FF and CR also end the text, a null `src` returns `NET_EINVAL`, and a null `dst` returns 1 or 0 without writing or setting errno. IPv6 converts as `sceNetInetPton` does - [sceNetMemoryAllocate and sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - allocations use the guest heap instead of the system network pool. Pool limits, its allocation accounting and free-time fill are not reproduced: a reused block allocated without flag 2 holds 0xdd bytes on the console and its previous contents here. Flag 4 bypasses the system pool reserve threshold; with no pool there is no threshold, so it allocates as other flags do. What the console returns once the pool is exhausted is unmeasured; AnyPS5 returns null without setting an errno. Measured on a PS5 (firmware 13.60): a size of 0 or less returns null and leaves `sceNetErrnoLoc` unchanged, flag 2 zero-fills, flags 1, 4, 8, 0x10, 0x100 and 0x80000000 had no visible effect, and every block of 1 to 65536 bytes was 64-byte aligned @@ -30,26 +31,34 @@ ### Functional - [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) prints nothing for a negative count, where the console prints `[LIBNET] usleep ` to standard output before failing with `EINVAL`. What an interrupted sleep returns is unmeasured: AnyPS5 returns the result of libkernel's `sceKernelUsleep`, a cancellation point. The signed 32-bit count (the upper half of the register is ignored) and the 0 result were measured on a PS5 (firmware 13.60) +- [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. Libkernel's own sockets go to its `ioctl`; other descriptors get `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 sampled when accept starts; changes between connection creation and accept are not modelled. Not measured on a console. +- [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. -- [sceNetConnect to AF_UNSPEC](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had +- [sceNetConnect to AF_UNSPEC](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux and on Windows a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again, and `sceNetGetsockname` reports that host address. On Windows a `sendto` on such a host socket to a non-loopback address fails with Winsock error 10051 (measured on the host socket, not through `sceNetSendto`), where FreeBSD sends it from the wildcard address. One bound to port 0 is bound again to the wildcard address with the port it had - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. Windows sockets stay non-blocking and the library waits for readiness until the stored timeout. Blocking stream sends return the queued byte count on timeout, or `EAGAIN` when no bytes were queued. - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) take `SO_SNDTIMEO` and `SO_RCVTIMEO` (0x1005, 0x1006) as a 16-byte `struct timeval`, as FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/uipc_socket.c#L2657-L2690) do, and Sony's 0x1105 and 0x1106 as an `int` of microseconds. A [console log of ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/issues/49) (firmware 12.20) shows receives timing out every 100 ms after it gave 0x1006 a `{0, 100000}` timeval. The errors for a value shorter than a timeval (`EINVAL`) or out of range (`EDOM`), and `sceNetGetsockopt` cutting the timeval to the caller's length, follow FreeBSD's `sooptcopyin` and `sooptcopyout` and are not measured on a console +- [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 -- [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. -- [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) set `TCP_NODELAY` (`IPPROTO_TCP`, 1) on the host socket of a stream socket and read it back as `TF_NODELAY` (4) or 0, as FreeBSD 11.4's [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1626-L1650) does. Every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_NOPUSH` and `TCP_KEEPIDLE` among them, which FreeBSD implements) fails with `ENOPROTOOPT`, and the `IPPROTO_TCP` level on a socket that is not a stream socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/ip_output.c#L943-L944) answers it. A set on a connection that FreeBSD's [`tcp_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1498-L1502) refuses with `ECONNRESET` (refused, reset or in `TIME_WAIT`) succeeds here because the host accepts it. A null value with a nonzero length fails with `EINVAL`, where FreeBSD's `kern_setsockopt` returns `EFAULT`, and a get into less than an `int` fails with `EINVAL`, where `sooptcopyout` truncates. None of this was measured on a console. +- [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`. +- [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) - `TCP_MAXSEG`, `TCP_NOPUSH`, `TCP_KEEPIDLE` and other TCP options remain unsupported (`ENOPROTOOPT`). `TCP_NODELAY` sets on refused, reset or `TIME_WAIT` sockets can succeed through the host where FreeBSD 11.4's [tcp_ctloutput](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1498-L1502) returns `ECONNRESET`. A null setter value returns `EINVAL` rather than `kern_setsockopt`'s `EFAULT`. The non-stream `IPPROTO_TCP` error (`EINVAL`) and `TF_NODELAY` flag readback follow FreeBSD, not PS5 measurements. +- [SO_TYPE and SO_ACCEPTCONN](../../../core/libs/prx/libSceNet/Export.cpp) read the guest socket type and listening flag, with bit-valued listening results and truncated output following FreeBSD 11.4's [sogetopt](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_socket.c#L2667-L2693), without PS5 verification. Null pointers retain the existing libSceNet `EINVAL` result instead of FreeBSD's pointer handling. - [sceNetGetSockInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reports 0 for the name, pid, policy, priority, queue lengths, virtual ports, state, bandwidth, drops and waits, sets no status flag except `SELF` (0x1) and `NONBLOCK` (0x200000), and reports the host's buffer sizes where none was set. An IPv6 socket fails with `EAFNOSUPPORT` and is not counted or listed; shadPS4 lists it with zero addresses. `flags` bits other than 0x40 fail with `EINVAL`, where shadPS4 ignores `flags`; 0x40 adds sockets in TIME_WAIT ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs)), but closed sockets are not kept. Otherwise the `s` below 0 count and list, the return value and `EINVAL` for a null `info` or an `n` below 1 follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/06e813ff8c8e2a6d81b02cabfcc9543c3debf78e/src/core/libraries/net/net.cpp#L1498-L1522). Not measured on a console. - [SO_SNDBUF and SO_RCVBUF validation](../../../core/libs/prx/libSceNet/Export.cpp) remains unverified on PS5, and positive sizes remain subject to host buffer limits. Nonpositive sizes fail with `EINVAL` before changing the native socket, following FreeBSD 11.4 [sosetopt](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_socket.c#L2494-L2509). - [sceNetSendmsg and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [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`. - [sceNetSocketClose, sceNetEpollControl and sceNetEpollWait](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - close and concurrent registration ordering are unmeasured on a console. Closed descriptors retire their registrations, and a wait discards snapshots invalidated by close, DEL or MOD; descriptor identity follows [Linux epoll(7)](https://man7.org/linux/man-pages/man7/epoll.7.html). ADD, MOD and DEL reject closed or invalid socket descriptors with `EBADF`, as [Linux epoll_ctl(2)](https://man7.org/linux/man-pages/man2/epoll_ctl.2.html) does; this error ordering is unmeasured on a console. Duplicate-descriptor alias lifetime is not modeled by this epoll backend. - [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - console ordering between competing readers and writers is unmeasured. The per-call behavior follows FreeBSD 11.4's `soreceive_generic` and `sosend_generic` (`sys/kern/uipc_socket.c`); `0x80` is FreeBSD's flag value (`sys/sys/socket.h`). On Windows native sockets stay non-blocking. `MSG_DONTWAIT` and guest `SO_NBIO` return one host transfer's result. Blocking stream sends queue the whole buffer with one deadline and return progress after a timeout or host error. SSL uses this path through the native owner, retaining the socket, its mode and timeouts. Readiness waits poll at most every 10 ms for local close, so idle blocked operations wake periodically. +- [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 - [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows and macOS until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). On macOS the host receive waits, but a receive-side `shutdown` does not wake it. A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the call waits for a datagram only for what is left of the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does - [sceNetResolverStartNtoa6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) resolves a host name to its first IPv6 address through the host resolver (`getaddrinfo` with `AF_INET6`) and writes the 16-byte address; the `(rid, hostname, addr, timeout, retry, flags)` signature follows `sceNetResolverStartNtoa`. A name with no IPv6 address fails with `RESOLVER_ENODNS` (`0x804101E1`) like a failed lookup; whether the console returns IPv4-mapped addresses instead is not verified. Timeout and retry are ignored as in `sceNetResolverStartNtoa`, and any nonzero flags throw because their meaning for this call is not known. Unverified on hardware. - [sceNetInetPton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - an IPv6 conversion with a null `dst` returns `NET_EINVAL` here, where it crashed a payload process on a PS5 (firmware 13.60). On that PS5 and on PS4 firmware 10.71 an IPv4 address ends at the first space, tab, LF, VT, FF or CR and the rest is ignored (`"1.2.3.4 x"` converts), and a failed conversion leaves the destination and `sceNetErrnoLoc` unchanged. On the PS5 the dotted IPv4 part of IPv6 text is read the same way (`"::ffff:1.2.3.4 x"` converts, `"::ffff:01.2.3.4"` fails), and an IPv4 conversion with a null `dst` returns 1 or 0 without writing. Leading whitespace, any other trailing byte (`0x01`, `0xa0`) and whitespace after hexadecimal IPv6 text fail +- [sceNetInetPton, sceNetInetNtop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) convert IPv6 text with libkernel's [inet_pton and inet_ntop](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp), so they take the same FreeBSD 11.4 text forms; beyond the PS5 measurements in the entry above, these are unverified on a console. IPv4 text keeps the host conversion. The checks and errors before the conversion are libSceNet's own and unchanged +- [sceNetResolverStartNtoa](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) ignores flag bits 0x1 and 0x10000 and does not apply the timeout or retry count: every lookup runs once, synchronously, through the host resolver. What those two bits do on a console is not known; the sceNetResolverAbort entry above gives shadPS4's names for them. A negative timeout or retry and any other flag bit fail with `EINVAL` before anything else is checked, as [prosper's reading of the libSceNet module](https://github.com/mattias800/prosper/blob/6c09a34a092a00898fcead5f855200ca7c368cd2/prosper/src/hle/net/hle_net.cpp#L579-L587) gives - [sceNetDhcpStart, sceNetAddrConfig6Start, sceNetDuplicateIpStart and sceNetInfoDumpStart](../../../core/libs/prx/libSceNet/Export.cpp) - argument widths and callback ABI are recovered. Address callbacks receive user data, a 32-bit event and a 32-bit error. DHCP configuration is mutable; the 512-byte IPv6 configuration is read-only. Their complete field layouts remain unknown. The two 32-bit thread parameters are stored but not consumed by this firmware thread wrapper, so their original names and units are unconfirmed. Information-dump options contain flags, an interval in seconds and a 64-byte path. All four exports remain unimplemented. +- [sceNetSetDnsInfo, sceNetSetDns6Info and sceNetGetDns6Info](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the resolver still uses the host's DNS servers; setters only store servers for the getters. Without an IPv6 override, the getter reports none. Counts follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0918faa0255e9816b795b62c9b09eaea51a1bc5e/src/core/libraries/net/net.cpp), not hardware measurements. All four DNS calls' initialization and argument errors match a reported [firmware 13.42 module probe](https://github.com/boykopovar/AnyPS5/pull/9506#issuecomment-6390574180): ENOTINIT first, then EINVAL for nonzero flags, null getter outputs or zero primary servers; null setters clear the override. Setters use a checked guest read and return EFAULT for unreadable non-null inputs, an AnyPS5 safety measure absent from the probed module. diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index add05f78df..ef536e17eb 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -8,11 +8,12 @@ - [sceNetCtlScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - argument registers and IPC transfer directions are recovered for the info, connection, scan, NAT, bandwidth and Wi-Fi queries. The connection configuration is 1464 bytes, scan options 88, BSSID output 96, scan info 112, IPv6 info 256, internal NAT info 196, bandwidth info 32, bandwidth-estimation info 28 and Wi-Fi info 48. Their field semantics still require the receiving service; incomplete declarations preserve this gap. Scan queries transfer a six-byte BSSID and use a 64-bit in/out buffer size. -- [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The DHCP host name (12), the PPPoE user name (13), the HTTP proxy server (20) and the Wi-Fi BSSID and SSID (5, 6) are empty strings, as that configuration sets none of them; the remaining Wi-Fi codes (7-10) and the HTTP proxy port (21) report 0; this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/network.cpp) and is not measured on hardware +- [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface with an IPv4 address, preferring the default-route one: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is always reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, and the Ethernet address is the one from `sceNetGetMacAddress`. A missing gateway or DNS server is `0.0.0.0`; the console value is unknown. The DHCP host name (12), PPPoE user name (13), HTTP proxy server (20) and Wi-Fi BSSID and SSID (5, 6) are empty strings and the other Wi-Fi codes (7-10) and the proxy port (21) are 0, as that configuration sets none of them; this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/network.cpp) and is not measured on hardware. One title (PPSA02225) calls code 12 at 0x1056f0f, 0xa0387f and 0xa26de5 and code 14 at 0x1056fa1; the meaning of the flag byte it checks is not shown. - [sceNetCtlGetInfoV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `Jy1EO5GdlcM`) - imported by PPSA08587. `(int code, SceNetCtlInfoV6* info)` with codes 1-5 (IP address, link-local address, default route, primary and secondary DNS) comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero), whose source is unknown; shadPS4 and fpPS4 only stub it. A null `info` answers `INVALID_ADDR`, a code outside 1-5 `INVALID_CODE`, and a host without an IPv6 address `NOT_CONNECTED`, as `sceNetCtlGetStateV6` reports it. The layout of each answer is unverified, so every code throws on a host with an IPv6 address - [sceNetCtlGetResult](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - `(int eventType, int* errorCode)` reports no error: event types 1-3 (disconnected to IP obtained, the numbering `sceNetCtlCheckCallback` delivers) write 0 to `errorCode`, as `sceNetCtlGetResultV6` does. The error code a console reports for each event, a disconnection's cause in particular, is unconfirmed. Callers found: PPSA20052 (Worms Armageddon: Anniversary Edition 01.003.000) calls it 2 s into boot, and PPSA24191 (Monster Hunter Wilds) without a network, with event 1 (disconnected) as its first call. A null `errorCode` answers `INVALID_ADDR` and other event types throw, as their error code on a console is unknown - [sceNetCtlGetResultV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `H5yARg37U5g`) - `(int eventType, int* errorCode)` comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/NetCtl.cs#L246-L251), whose source is unknown; shadPS4 only stubs it. Callbacks registered with `sceNetCtlRegisterCallbackV6` receive no event, so event types 1-3 write 0 to `errorCode`; what a console writes before the first event is unmeasured. A null `errorCode` answers `INVALID_ADDR`, as `sceNetCtlGetResult` does, and other event types throw, as their error code on a console is unknown - [sceNetCtlRegisterCallbackForNpToolkit, sceNetCtlUnregisterCallbackForNpToolkit, sceNetCtlCheckCallbackForNpToolkit](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - a table of eight callbacks apart from the IPv4 and IPv6 ones, with the `(int eventType, void* arg)` callback, follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/net_ctl_obj.h), whose NpToolkit check delivers the same events to that table. The three share the register, unregister and dispatch of `sceNetCtlRegisterCallback`, `sceNetCtlUnregisterCallback` and `sceNetCtlCheckCallback`, so they return the same initialization, address, slot-limit and ID errors, and the check reports the state `sceNetCtlCheckCallback` does. Whether the console keeps this table apart, and what `sceNetCtlCheckCallback` does with it, is unverified on hardware +- [sceNetCtlGetStateIpcInt, sceNetCtlGetStateV6IpcInt, sceNetCtlGetResultIpcInt, sceNetCtlGetResultV6IpcInt, sceNetCtlGetInfoIpcInt, sceNetCtlGetInfoV6IpcInt, sceNetCtlRegisterCallbackIpcInt, sceNetCtlUnregisterCallbackIpcInt, sceNetCtlRegisterCallbackV6IpcInt and sceNetCtlUnregisterCallbackV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answer as their public twins, whose recovered argument registers they share: the getters return and write what `sceNetCtlGetState`, `GetStateV6`, `GetResult`, `GetResultV6`, `GetInfo` and `GetInfoV6` do, throws included, and the callback forms use the IPv4 and IPv6 tables, as no IpcInt check is exported to deliver a table of their own. This rests on the matching call shapes, not on a measurement; whether the internal client answers a null pointer or a bad code before its service does is unknown - [ApRp exports](../../../core/libs/prx/libSceNetCtl/Export.cpp) are constant-success functions in the supplied native module and consume no arguments. No callers were found in the supplied system-library decompilations, so unused callback, output and configuration argument types remain unresolved. Protected stub bodies are unchanged. ### Functional diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 5997a09d48..453bb09f88 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -2,6 +2,8 @@ ### Unknown function info +- [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. - [NGS2 system and rack handle errors](../../../core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp) use `0x804A8201` and `0x804A8202`, following the native handle-type error helper recorded in [prosper's PS5 module analysis](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp). [sceNgs2RackGetVoiceHandle](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) resolves the rack before checking the output pointer, matching the export in firmware 1.00 through 12.70 reported in [#5916](https://github.com/boykopovar/AnyPS5/pull/5916#issuecomment-6172832590). Other exports retain their existing validation order; complete invalid-input precedence and console execution remain unverified. @@ -14,9 +16,10 @@ - [NGS2 submixer UserFx](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (control `0x20000004`) invokes the title's callback after input mixing on every playing grain, including silence. The 40-byte control and 48-byte process context follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/ngs2/ngs2_submixer.h). PPSA03747 installs a peak-meter callback; its code confirms the channel pointers, user data 0, channel count, grain size and sample-rate offsets and returns 0. Other user data words are preserved. Flags are assumed to be 1 after registration/restart and 0 thereafter, following UserFx2's first-process convention; this callback does not inspect them. Null handlers disable the effect, setup clears it, and nonzero callback results throw. These lifecycle and error semantics are unverified on console hardware. - [NGS2 voice callback after setup](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a sampler or mixer setup clears the voice callback with the rest of the voice state. PPSA18216 registers a block-end callback for a streamed music voice, later sets the same voice up again for one-shot ATRAC9 blocks with user data 0, and registers the callback again before it streams again; its callback dereferences the block's user data, so it must not run for the one-shot blocks. A setup made from inside the voice's own callback clears that callback as well, so a callback that sets its voice up again and queues the next block runs once instead of for every block; PPSA18216 never sets a voice up from inside its callback, so that ordering is unverified. Unverified on console hardware. - [NGS2 ATRAC9 streaming](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) keeps waveform duration and skip counts across compressed input blocks. The shared `APPEND` flag (2), called `ONLY_DATA` in the compressed-data reference, ignores an ATRAC9 continuation block's sample, skip and repeat counts, following [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/bb98f26433fa9e711c219aac1711fe8ecba5bf3a/src/libs/ngs2.cpp); PPSA03747 confirms a 262044-byte initial buffer declaring 11310532 samples, followed by 16384-byte buffers with flags 3. Incomplete superframes are retained until more input arrives. Input-block completion callbacks allow refills; the final callback of a logical waveform waits for its buffered PCM to drain, preserving the existing complete-block repeat behaviour. This buffering, callback timing, decoded-byte accounting and starvation silence are unverified on hardware. A closed ATRAC9 stream ending before its declared samples throws; fragmented ATRAC9 waveforms with repeats are unsupported. PCM streaming retains its existing append validation and early-close behaviour. -- [NGS2 mastering voice gain](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the param id 0x30000004 is taken from PPSA22520 and the layout from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2MasteringVoiceGainParam`; no reference gives the units or range. The levels are assumed to be linear factors applied when the mastering voice is mixed into the render buffer, so the voice's own samples stay unscaled; the LFE level applies to channel 3 of 6- and 8-channel voices, assuming the L R C LFE order of the AudioOut2 entries above, and the full-band level to the rest. Negative levels are accepted, and a new setup resets both levels to 1 +- [NGS2 mastering voice gain](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a NaN sample in a PCM_I16L render buffer throws, because PS5 firmware 3.20 and 4.03 were measured only into float buffers, where a NaN gain gives NaN samples. Measured there: param 0x30000004 (id seen in PPSA22520) sets linear full-band and LFE factors on the mastering output, applied from the next render, flat for the first gain a voice receives and ramped over the next grain in eight lanes after that, which this change does not implement; each is clamped to [-32, 32] with NaN kept, and every value returns 0. The LFE level acts on channel 3 of 6- and 8-channel voices and the full-band level on every other channel, 1- and 2-channel voices included; a mastering setup resets both to 1, and STOP_IMM plus PLAY keeps them - [NGS2 stereo mastering voice into a surround buffer](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - PPSA21402 renders a 2-channel mastering voice into a 6- or 8-channel render buffer, which the mixing stage rejected. The console's conversion is unknown: the left and right channels are assumed to go to the first two buffer channels (front left and right) and the remaining channels stay silent, with no centre, LFE or surround content. Any other channel count mismatch still throws - [NGS2 voices without a format](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a sampler voice whose setup has no format (all zero) accepts an add of no waveform blocks without `APPEND`, which only applies `RESET` and the `CONTINUE` state, and matrix levels with no levels, which clear that matrix. PPSA36591 sets such a voice up, adds no blocks and sets no levels when a button is pressed at its title screen. Whether the console returns success or an error there is not measured; blocks with data, `APPEND` and an empty matrix on a voice with a format are still refused +- [NGS2 play without setup](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - differs from PS5 firmware 3.20 and 4.03 `libSceNgs2.native.sprx` in two cases: a set-up voice given an add of no waveform blocks and played keeps state flags 1 there for as long as it was watched and only until the next render here; an add of no blocks to a voice that already played and ended returns `0x804A8054` there and succeeds here. A `PLAY` on a voice with no channels (never set up; an all-zero setup leaves an earlier setup untouched) returns `0x804A8351` (`SCE_NGS2_ERROR_INVALID_VOICE_ACCESS`) from Control and RunCommands, leaves it idle and skips the rest of the list, as that module does for every rack; PPSA03061 (Void Terrarium) frees its sound slots that way - [NGS2 silent waveform blocks](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native): flag `0x10` (named `SILENCE` here; no public reference names it) lets a waveform block declare samples without data. PPSA04867 adds one block with flags `0x11`, `data_size` 0 and `num_samples` 16385, then another with flags `0x11` and a fully specified 0x800-byte, 1024-sample mono block, which takes the ordinary path. A dataless block under the flag outputs `num_samples` frames of silence, reports them through `decoded_data_size`, and finishes like a complete block; a dataless block without the flag still throws as not fitting its data. The flag's real name, its interaction with ATRAC9 and repeats, and the console's `waveform_data` for silent blocks are unverified on hardware - [NGS2 streaming PCM waveforms](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - PPSA21402 streams its music by adding one block whose `num_samples` is the whole track and whose `data_size` is a 16 KiB first piece, with `CONTINUE`, and then adds each following piece with flag `0x2` (called `APPEND` here) and the same `num_samples`. No reference describes flag `0x2`. A 16-bit PCM block without skipped samples or repeats whose `num_samples` exceed its data is assumed to start such a stream; flag `0x2` is assumed to append the block's data to the last streaming block, ignoring its `num_samples`; the voice is assumed to output silence without advancing while it has played all appended data and `CONTINUE` is set; and the waveform is assumed to end at `num_samples` or, once `CONTINUE` is cleared, at the end of the appended data. `sceNgs2VoiceGetState` reports the bytes played as `decoded_data_size`, which the title uses to release its queued data, and the read position as `waveform_data`. Appending to a block that is not a stream, or other formats, skipped samples, repeats and partial frames, throw - [NGS2 matrix levels](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - levels are clamped to -4..4 and NaN is kept, as the PS4 firmware 11.00 `libSceNgs2.sprx` stores them; not yet confirmed on the PS5 library. That library rejects a level count of 0 and keeps the matrix; the PS5 error code is unknown, so a count of 0 throws, except on a sampler voice without a format, where it clears the matrix (see NGS2 voices without a format). `sceNgs2VoiceRunCommands` is not in that library, so its matrix levels command is assumed to clamp the same way @@ -35,7 +38,7 @@ - [sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A null user data pointer throws. `sceNgs2SystemLock` and `sceNgs2SystemUnlock` follow [prosper's notes](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4367-L4384) on the PS5 module: Lock takes a per-system lock and counts nested holds, Unlock releases one hold from any thread and returns 0 without a hold, and `sceNgs2SystemRender` only tries the lock: while another thread holds it, Render clears one grain of each output, leaves the render count and voices unchanged and returns 0. The other calls do not wait for the system lock. Peace Walker (PPSA18216) holds it while it spins on a flag that its audio thread clears only after `sceNgs2SystemRender` and eight `sceNgs2VoiceGetStateFlags` calls, so on the console neither can wait for it. Whether the console's voice and rack calls take the lock at all is not known. `sceNgs2RackLock` still holds the library's lock until `sceNgs2RackUnlock`, as before - [sceNgs2GetWaveformFrameInfo](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - not measured on a console; shadPS4 only stubs it, and prosper does not cover the call. The outputs are taken to be the frame fields of the parsed waveform info, which [prosper's reading of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L224-L231) fills for ATRAC9 with the superframe bytes and samples, the frames per superframe and a one-frame delay, whatever the `fact` chunk says. `PCM_I16L` answers channels * 2 bytes, 1 sample, 1 unit and no delay, as for parsed 16-bit PCM, itself assumed. Other types throw. Null outputs return `INVALID_OUT_ADDRESS` and an ATRAC9 format the codec does not accept `INVALID_WAVEFORM_FORMAT`; the codes are unverified - [sceNgs2RackSetUserData, sceNgs2RackGetUserData](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - the signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L418-L421) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.cpp#L86-L111), which only logs them; neither gives an error. A handle that is not a live rack answers `INVALID_RACK_HANDLE` before a null user data pointer is checked, and the null pointer throws, as in `sceNgs2SystemGetUserData`. A new rack starts with 0. Not measured on a console -- [sceNgs2RackGetInfo](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - field order from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2RackInfo` with the 64-byte name of the other info structures (216 bytes); `type` is assumed to be the rack id divided by 0x1000, `min_grain_samples` 64 (the system minimum) and `state_flags` 1; `uid` comes from a per-process rack counter, `render_count` is the owning system's render count and `active_voice_count` counts voices that are not empty (paused and stopped voices included). The last process ratio and tick and `active_channel_work_count` are left zero +- [sceNgs2RackGetInfo](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - field order from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2RackInfo` with the 64-byte name of the other info structures (216 bytes); `type` is assumed to be the rack id divided by 0x1000, `min_grain_samples` 64 (the system minimum) and `state_flags` 1; `uid` comes from a per-process rack counter, `render_count` is the owning system's render count and `active_voice_count` counts voices that are not empty (paused and stopped voices included). The last process ratio and tick and `active_channel_work_count` are left zero. One title (PPSA13356, call at 0xB4F74) passes a zeroed 0xe8-byte buffer and the size 0xe8, and reads a 32-bit count at offset 0xa4 only when the result is 0. This implementation returns the out-size error for that size; neither the 216-byte layout nor a 232-byte structure is verified on the console. - [NGS2 system option checks](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - not verified on a console. `sceNgs2SystemQueryBufferSize`, `sceNgs2SystemCreate` and `sceNgs2SystemCreateWithAllocator` return `0x804A8013` (size), `0x804A8311` (max grain, a multiple of 64 up to 2048), `0x804A8310` (grain, a multiple of 64 up to the max), `0x804A8051` (one of ten sample rates) and `0x804A8052` (1 to 37 channels) in that order, following [prosper's PS5 module analysis](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/audio/hle_audio.cpp#L4151-L4226). The four job scheduler pointers are neither inspected nor used. - [sceNgs2SystemSetGrainSamples, sceNgs2SystemSetSampleRate](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - a grain that is not a multiple of 64 is accepted, where the module returns `0x804A8310`, because the NGS2 tests render 8-sample grains. Changing the rate while a sampler filter is enabled throws, as the filter coefficients were computed for the old rate. After the handle check, a grain of 0 or above the system's maximum returns `0x804A8310`, and a rate other than 11025, 12000, 22050, 24000, 44100, 48000, 88200, 96000, 176400 or 192000 returns `0x804A8051`, per [prosper's reading of the module's command validators](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/audio/hle_audio.cpp#L4355-L4399); not measured on a console - [sceNgs2VoiceQueryInfo](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw @@ -44,9 +47,10 @@ - [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` - [sceNgs2VoiceControl patch](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a destination rack handle with input id 0 resolves to that rack's first voice; this matches the Dead Cells call in #4334, but selection for racks with multiple voices has not been checked against a console. Nonzero destination input ids still throw. `sceNgs2VoiceGetPortInfo` reports the resolved voice handle and zero delay. +- [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. Voices still play only 16-bit PCM and ATRAC9 - [sceNgs2ParseWaveformFile](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw - [sceNgs2VoiceControl sampler filter](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown -- [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. A speaker count of 0 without angles, which the firmware accepts, a non-finite speaker angle and a speaker angle equal to the unit angle, on which that library loops forever, throw. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason +- [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - inputs on which the PS5 `libSceNgs2.native.sprx` of firmware 3.20 and 4.03 did not return within 60 s throw; whether it returns at all for them is unknown. They are an infinite speaker or param angle, an angle too large for a turn to change it, a speaker angle that wraps to exactly the unit angle, and a param angle that wraps onto the first speaker plus one turn, such as one float below the first speaker in radians - [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomApply` is now a minimal pass-through that follows [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `Ngs2GeomApply` (shadPS4 only stubs it): pitch 1, source position, volume the larger of the source minimum and maximum level, and a diagonal of the source maximum level (1 when it is not positive) over `matrix_format` channels (0 is two, at most 8). The listener and `flags` are ignored, so source attenuation, distance rolloff, panning and Doppler stay unimplemented; neither KytyPS5 nor this has been measured on a PS5, and a null output address returns `INVALID_OUT_ADDRESS` where KytyPS5 exits - [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) builds an orthonormal world-to-listener transform. The row-major matrix, front-axis priority and flag 1 for right-handed input follow [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig); they have not been measured on PS5 hardware. Velocity stays in world coordinates and positive finite sound speed is preserved. Orientation vectors are normalised and up is made perpendicular to front, so non-unit and slightly tilted vectors are accepted. When front and up are parallel, up is replaced by `(0,1,0)` or `(1,0,0)` so the basis stays defined: PPSA15552 (Dead Cells) passes `orient_front={0,0,1}` with `orient_up={0,0,1}` after moving the listener, which PS5PCEM rejects; the console error code is unknown. Zero orientation vectors, nonfinite input and an unrepresentable translation still throw without changing the output; console error codes are unknown, and a nonpositive sound speed throws where PS5PCEM and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) default it to 343. KytyPS5 always writes the identity matrix, so the transform rests on PS5PCEM plus that Dead Cells case. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334. This does not implement source attenuation, panning or Doppler in `sceNgs2GeomApply`. - [NGS2 custom sampler rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4001) - implements bounded support for the custom sampler rack and its built-in module 0x13 (Custom Voice Filter), unblocking PPSA15552 (Dead Cells) startup. Structure layouts come from [shadPS4's ngs2_custom.h](https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h) (OrbisNgs2CustomVoiceFilterParam, 40 bytes) and live trace correlation from [prosper's hle_audio.cpp](https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp) (0x40001300 command and parameters like 10000.0f and 0.707f). Routes parameter updates via the 0x40001300 command family into the Biquad filter engine. Filter type 0 clears the destination filter; type 1 configures the RBJ low-pass biquad frequency, Q and level. Other filter types and unmodeled configurations throw. diff --git a/docs/dev/debt/libSceNotification.md b/docs/dev/debt/libSceNotification.md new file mode 100644 index 0000000000..6245484e54 --- /dev/null +++ b/docs/dev/debt/libSceNotification.md @@ -0,0 +1,6 @@ +# libSceNotification technical debt + +### Unknown function info + +- [sceNotificationSend, sceNotificationSendById, sceNotificationShowPsButtonPersistentBanner, sceNotificationHidePsButtonPersistentBanner](../../../core/libs/prx/libSceNotification/Export.cpp) (libSceNotification) - notification sending and banner presentation interfaces throw through `NotImplemented_nid_no_patch`. Native notification service IPC and parameter structures remain unimplemented. +- [sceNotificationSend](../../../core/libs/prx/libSceNotification/Export.cpp) (libSceNotification) - what the console shows, checks and returns is not measured. The parameters are the ones homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) declares, `int sceNotificationSend(int userId, bool isLogged, const char* payload)`: its `notify` sample passes the system user (`0xFE`), `true` and a JSON document whose `rawData` names a `viewTemplateType` such as `InteractiveToastTemplateB` and holds the `viewData` to show, and exits with the result. Closes when: measured on a PS5. diff --git a/docs/dev/debt/libSceNpAuth.md b/docs/dev/debt/libSceNpAuth.md index e918766151..83f702e202 100644 --- a/docs/dev/debt/libSceNpAuth.md +++ b/docs/dev/debt/libSceNpAuth.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceNpAuthAbortRequest, sceNpAuthGetAuthorizationCode, sceNpAuthGetAuthorizationCodeA, sceNpAuthGetAuthorizationCodeV3, sceNpAuthGetIdToken, sceNpAuthGetIdTokenA, sceNpAuthGetIdTokenV3, sceNpAuthPollAsync, sceNpAuthWaitAsync](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - request tracking, `ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND` (0x80550306) on unknown IDs, `ORBIS_NP_AUTH_ERROR_INVALID_ID` (0x80550307) on non-async or unprocessed requests in Poll/Wait, `ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT` (0x80550301) on null arguments, and `ORBIS_NP_AUTH_ERROR_ABORTED` (0x80550304) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp) (PS4); the A calls forward to their V3 counterparts, which in shadPS4 ([AuthorizationCode A](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L189-L193), [V3](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L195-L199); [IdToken A](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L375-L378), [V3](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L380-L383)) call the same helper and differ only in a version flag; async requests defer `SCE_NP_ERROR_SIGNED_OUT` to Poll/Wait while sync requests fail immediately; PSN is not contacted. Call sites in three disassembled titles (PPSA02225, PPSA15246, PPSA30601; static reading only) show the V3 parameter layouts: the code request parameter is 0x20 bytes (size, a 32-bit value that the titles take from their user object, a pointer to a client-ID-like string, a pointer to a constant string) and the call is `(reqId, param, code[128], int* issuerId)`; the ID token request parameter is 0x28 bytes (size, the same kind of 32-bit value, a pointer to a client-ID-like string, a pointer to a second buffer (256 bytes in one title) and a pointer to a constant string) and the call is `(reqId, param, token)`. The client-ID-like string is 36 characters plus a terminator in one title and a 128-byte buffer in two. In two titles the calls follow `sceNpAuthCreateAsyncRequest` (whose parameter holds only a size of 0x18) and are polled with `sceNpAuthPollAsync` (one title once per frame, one in a blocking loop of up to 40 polls 100 ms apart); the third title is a synchronous caller that takes the request ID from an argument-less call. All three call `sceNpAuthDeleteRequest` once the request has finished or failed (immediately after the call in the synchronous title, after a poll return of 0 in the per-frame title, on every path in the blocking one). The titles treat a poll return of 0 as finished and any negative result as a failure without looking at the specific code; one title, after 40 polls that still return non-zero, treats a return of exactly 1 as a timeout and calls `sceNpAuthAbortRequest`, and reports 0x80550301 and 0x80550306 from Poll as an invalid result pointer and an unknown request ID. The size fields are not checked here; the console's size checks and the behaviour of the calls on a console are unverified +- [sceNpAuthAbortRequest, sceNpAuthGetAuthorizationCode, sceNpAuthGetAuthorizationCodeA, sceNpAuthGetAuthorizationCodeV3, sceNpAuthGetIdToken, sceNpAuthGetIdTokenA, sceNpAuthGetIdTokenV3, sceNpAuthPollAsync, sceNpAuthWaitAsync](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - request tracking, `ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND` (0x80550306) on unknown IDs, `ORBIS_NP_AUTH_ERROR_INVALID_ID` (0x80550307) on non-async or unprocessed requests in Poll/Wait, `ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT` (0x80550301) on null arguments, and `ORBIS_NP_AUTH_ERROR_ABORTED` (0x80550304) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp) (PS4); the A calls forward to their V3 counterparts, which in shadPS4 ([AuthorizationCode A](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L189-L193), [V3](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L195-L199); [IdToken A](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L375-L378), [V3](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L380-L383)) call the same helper and differ only in a version flag; async requests defer `SCE_NP_ERROR_SIGNED_OUT` to Poll/Wait while sync requests fail immediately; PSN is not contacted. Call sites in three disassembled titles (PPSA02225, PPSA15246, PPSA30601; static reading only) show the V3 parameter layouts: the code request parameter is 0x20 bytes (size, a 32-bit value that the titles take from their user object, a pointer to a client-ID-like string, a pointer to a constant string) and the call is `(reqId, param, code[128], int* issuerId)`; the ID token request parameter is 0x28 bytes (size, the same kind of 32-bit value, a pointer to a client-ID-like string, a pointer to a second buffer (256 bytes in two titles: a 256-byte copy in one, a 256-byte zeroed area holding a 16-character string in the other) and a pointer to a constant string) and the call is `(reqId, param, token)`. The client-ID-like string is 36 characters plus a terminator in one title and a 128-byte buffer in two. In two titles the calls follow `sceNpAuthCreateAsyncRequest` (whose parameter holds only a size of 0x18) and are polled with `sceNpAuthPollAsync` (PPSA15246 at 0x4feefd with one non-blocking poll per run of a title function, PPSA30601 at 0x16edb90 in a blocking loop that polls once and then up to 40 more times, each after a 100 ms sleep); the third title is a synchronous caller that takes the request ID from an argument-less call. All three call `sceNpAuthDeleteRequest` once the request has finished or failed (immediately after the call in the synchronous title, after a poll return of 0 in the non-blocking one, on every path in the blocking one). The titles treat a poll return of 0 as finished and any negative result as a failure without looking at the specific code; one title, when the 41st poll still returns non-zero, treats a return of exactly 1 as a timeout and calls `sceNpAuthAbortRequest`, and reports 0x80550301 and 0x80550306 from Poll as an invalid result pointer and an unknown request ID. The size fields are not checked here; the console's size checks and the behaviour of the calls on a console are unverified - [sceNpAuthPollAsync and sceNpAuthWaitAsync](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - PS5 abort-before-start behavior is unmeasured. Poll/Wait keep returning `ORBIS_NP_AUTH_ERROR_INVALID_ID` without changing output after a pre-start abort, including subsequent rejected getter calls, following [shadPS4 StartRequest and FindAsyncRequest](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp). diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index da67369967..be7ac29c21 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -2,9 +2,8 @@ ### Unknown function info -The 35 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. +The 34 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. -- [sceNpCloseEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `+nmn+Z0nWDo`) - 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`. - [sceNpCloseSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `8hPzfjZzV88`) - 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`. - [sceNpGetNavSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `QmDEFikd3VA`) - 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`. - [sceNpIntGetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `G6qytFoBJ-w`) - 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`. @@ -54,6 +53,6 @@ The 35 declarations below use names mapped from the local `libSceNpCommon.sprx` - [The string-to-integer exports](../../../core/libs/prx/libSceNpCommon/Strings.cpp) clear `errno` before parsing; the firmware reads whatever `errno` held before the call when the result saturates. - [The integer-to-string exports](../../../core/libs/prx/libSceNpCommon/Strings.cpp) return `0x80558008` when the hexadecimal prefix does not fit; the firmware writes the prefix without checking the size. [The base64 decoders](../../../core/libs/prx/libSceNpCommon/Strings.cpp) treat bytes above 0x7F as invalid; the firmware indexes its table with them as signed values. [sceNpUtilJsonGetOneChar](../../../core/libs/prx/libSceNpCommon/Strings.cpp) checks that a `\u` escape and a surrogate pair fit in the input; the firmware reads past it. - [sceNpUtilGetNpDebug, sceNpUtilGetNpTestPatch, sceNpUtilGetTrcNotify, sceNpUtilGetIgnoreNpTitleId, sceNpUtilGetFakeDisplayNameMode, sceNpUtilGetWebTraceSetting, sceNpUtilGetWebApi2FakeRateLimit, sceNpUtilGetWebApi2FakeRateLimitTarget, sceNpUtilGetFakeRateLimit, sceNpUtilGetDbgCommerce, sceNpUtilGetSystemLanguage, sceNpUtilGetEnv, sceNpUtilGetDateSetAuto and sceNpUtilGetShareTitleCheck](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) read system and debug registry keys through libSceRegMgr, whose `sceRegMgrNonSysGetInt` and string reads still throw; they throw until those exist. -- [sceNpOpenSema, sceNpCloseSema and sceNpCloseEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) forward to the libkernel open and close calls for named semaphores and event flags, which still throw. +- [sceNpOpenSema and sceNpCloseSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) forward to the libkernel open and close calls for named semaphores, which still throw. - [sceNpSetPlatformType, sceNpIntGetPlatformType, sceNpIntSetPlatformType, sceNpUtilCanonicalizeNpIdForPs4, sceNpUtilCanonicalizeNpIdForPsp2, sceNpUtilJidToNpId and sceNpGetNavSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) depend on console platform tags, network domain names or a version string of the console; they throw. - [sceNpJsonParse, sceNpJsonParseInit, sceNpJsonParseBuf, sceNpJsonParseBufInit, sceNpJsonParseEx, sceNpJsonParseExInit and the sceNpServerErrorJson exports](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) are a streaming, schema-checked JSON parser whose event and error rules are not yet specified; they throw. diff --git a/docs/dev/debt/libSceNpCppWebApi.md b/docs/dev/debt/libSceNpCppWebApi.md index ee3994c483..4f28810e85 100644 --- a/docs/dev/debt/libSceNpCppWebApi.md +++ b/docs/dev/debt/libSceNpCppWebApi.md @@ -5,7 +5,7 @@ - [sce::Np::CppWebApi::Common](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the `finish` overrides of `TransactionBase<...>` (13 instantiations), `DownStreamTransaction<...>` (2 instantiations) and `Vector>::pushBack` return success without doing work; a failed request never produces a transaction to finish. `Transaction<...>::setOnFinishedCallback` and `TransactionBase<...>::setResponseInformationOption` (5 instantiations each, the `UserProfile::V1`, `ProfanityFilter::V2` and `CommunicationRestrictionStatus::V3` responses) return success without storing the callback or the option: `start` fails, so neither is ever used. The constructors and destructors of the same five `Transaction<...>` instantiations succeed without keeping state - [sce::Np::CppWebApi::Leaderboards::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - request-body and parameter bookkeeping succeeds without storing state: `GetRankingRequestBody::set{StartSerialRank,Group,Limit,Users,Offset}`, `RecordScoreRequestBody::set{Comment,SmallData,NeedsTmpRank,ComparedDateTime,PcId}`, `User::{setAccountId,setPcId}`, `BoardsApi::ParameterToGetBoardDefinition::{initialize,terminate}`, `RecordApi::ParameterToRecordLargeData::{initialize,terminate}`, `RecordApi::ParameterToRecordScore::{initialize,setxPsnAtomicOperation,terminate}`, `ViewApi::ParameterToGetLargeDataByObjectId::{initialize,terminate}`, `ViewApi::ParameterToGetRanking::{initialize,setgetRankingRequestBody,terminate}` - [sce::Np::CppWebApi::SessionManager::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - parameter bookkeeping succeeds without storing state: `PlayerSessionsApi::ParameterToCreatePlayerSessions::{ctor,dtor,initialize}`, `PlayerSessionsApi::ParameterToGetPlayerSessionInvitations::{ctor,dtor,initialize,setfields,setinvitationInvalidFilter}`, `PlayerSessionsApi::ParameterToJoinPlayerSessionAsPlayer::{ctor,dtor,initialize,terminate}`, `PlayerSessionsApi::ParameterToLeavePlayerSession::{ctor,dtor,initialize}`, `RequestPlayerSession::set{MaxSpectators,InvitableUserType,JoinableUserType,ExclusiveLeaderPrivileges}`, `RequestPlayerSessionPlayer::setPushContexts` -- [sce::Np::CppWebApi::TitleCloudStorage::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - `DataApi::ParameterToDownloadData::{initialize,terminate}` succeed without storing state +- [sce::Np::CppWebApi::TitleCloudStorage::V1 and InGameCatalog::V5](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - `DataApi::ParameterToDownloadData`, `DataApi::ParameterToGetMultiDataStatusesByUser` and `ContainerApi::ParameterToGetContainer` keep only whether they are initialized. `initialize`, `terminate` and the string setters return the codes of the module bundled with PPSA21564: 0x80553503 for a null `LibContext` or string, 0x80553502 for a `LibContext` that `Common::initialize` did not set or a parameter not yet initialized, 0x8055350f for a second `initialize` and 0x80553510 for `terminate` without one. They store none of the values, and `ParameterToGetContainer::set{sort,offset,limit,serviceLabel}` store nothing either: only a request would read them - [sce::Np::CppWebApi::UserProfile::V1, ProfanityFilter::V2, CommunicationRestrictionStatus::V3](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - parameter bookkeeping succeeds without storing state: `FriendsApi::ParameterToGetFriends`, `PresenceApi::ParameterToGetBasicPresences`, `BlocksApi::ParameterToGetBlockingUsers`, `ProfanityApi::ParameterToFilterProfanity` and `CommunicationRestrictionStatusApi::ParameterToGetCommunicationRestrictionStatus`, each `{ctor,dtor,initialize,terminate}` ### Unknown function info diff --git a/docs/dev/debt/libSceNpGameIntent.md b/docs/dev/debt/libSceNpGameIntent.md index 622b40a2c2..cc0daa50c5 100644 --- a/docs/dev/debt/libSceNpGameIntent.md +++ b/docs/dev/debt/libSceNpGameIntent.md @@ -2,11 +2,12 @@ ### Silent stubs -- [sceNpGameIntentVshInitialize, sceNpGameIntentVshTerminate](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - VSH initialization and termination return 0 without performing shell operations, matching `sceNpGameIntentInitialize` and `sceNpGameIntentTerminate` -- [sceNpAppLauncherInitialize, sceNpAppLauncherTerminate](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - app-launcher lifecycle returns success without tracking launch state; `sceNpAppLauncherInitialize` accepts its init parameter without validation, following the silent-stub precedent of `sceNpGameIntentInitialize` — a title calls it early in boot, and returning success lets it continue +- [sceNpGameIntentVshInitialize, sceNpGameIntentVshTerminate](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - VSH initialization and termination return 0 without performing shell operations +- [sceNpAppLauncherInitialize, sceNpAppLauncherTerminate](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - app-launcher lifecycle returns success without tracking launch state; `sceNpAppLauncherInitialize` accepts its init parameter without validation — a title calls it early in boot, and returning success lets it continue ### Unknown function info +- [sceNpGameIntentReceiveIntent, sceNpGameIntentGetPropertyValueString](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - firmware 13.42 traces report `0x80553802` before `sceNpGameIntentInitialize` (precedence over argument validity), and `0x80553804` for invalid arguments (null pointer, empty key, or zero buffer size); `sceNpGameIntentTerminate` resetting initialized state is modeled lifecycle behavior rather than hardware-verified. `sceNpGameIntentReceiveIntent` reportedly requires `info->size == 0x4140` on firmware 13.42, while AnyPS5 `NpGameIntentInfo` is `0x560` bytes (matching fpPS4) and set by `sceNpGameIntentInfoInit`, so initialized objects are rejected by the receiver's measured size guard. Complete retail layout, output field offsets, and retail behavior of `sceNpGameIntentInfoInit` are unverified; expanding the type with unverified reserved bytes is not an evidence-backed solution. - [sceNpAppLauncherLaunchApp, sceNpGameIntentGetUdsNpComId, sceNpGameIntentLaunchApp, sceNpGameIntentLaunchApp2, sceNpGameIntentNotifyAppLaunched, sceNpSessionServiceGetJoinedPartySession, sceNpSessionServiceGetRunningGamePlayerSession and sceNpSessionServiceSendPlayerSessionInvitations](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpGameIntent.c). Call sites in a title's executable give a partial prototype: `sceNpAppLauncherLaunchApp` takes `(user_id, uri struct, context)` and returns int — throws because no launcher-service semantics are derivable. The rest are declared without parameters solely to resolve known imports and make no ABI claim. - [sceNpGameIntentInitParamInit](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - struct layout matches [fpPS4](https://github.com/red-prig/fpPS4/blob/master/src/np/ps4_libscenpgameintent.pas#L17) `SceNpGameIntentInitParam`; setting `size`, zeroing reserved bytes, and throwing on null pointer follow Sony SDK `*ParamInit` conventions but are unverified on retail hardware -- [sceNpGameIntentInfoInit](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - struct layout matches [fpPS4](https://github.com/red-prig/fpPS4/blob/master/src/np/ps4_libscenpgameintent.pas#L28) `SceNpGameIntentInfo`; setting `size`, `user_id = -1`, zeroing fields, and throwing on null pointer match `sceNpGameIntentReceiveIntent` defaults but are unverified on retail hardware +- [sceNpGameIntentInfoInit](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - struct layout matches [fpPS4](https://github.com/red-prig/fpPS4/blob/master/src/np/ps4_libscenpgameintent.pas#L28) `SceNpGameIntentInfo`; setting `size = 0x560`, `user_id = -1`, zeroing fields, and throwing on null pointer follow fpPS4 defaults, but reported firmware 13.42 behavior requires `0x4140` for `sceNpGameIntentReceiveIntent`, so the helper's `0x560` size does not match retail receiver defaults; the complete retail structure layout and retail behavior of `sceNpGameIntentInfoInit` remain unverified diff --git a/docs/dev/debt/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index 0595fc92c4..fdf72c3530 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -24,7 +24,8 @@ - [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 - [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)); `sceNpUnregisterStateCallbackA` returns `INVALID_ARGUMENT` for an id outside 1..8 and `CALLBACK_NOT_REGISTERED` for an empty slot; the legacy [sceNpRegisterStateCallback](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L981-L1005) keeps one callback with the same errors. Each registration gets `SIGNED_OUT` for the initial user (0x10000000) once, from the next `sceNpCheckCallback` on the calling thread. For the A form this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3959-L3974), whose [sceNpRegisterStateCallbackA](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4021-L4034) marks the registration pending. KytyPS5 never calls the legacy callback ([sceNpRegisterStateCallback](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4009-L4019) stores nothing); it is called as `(user, SIGNED_OUT, nullptr, userdata)`, as in shadPS4, which passes an NpId only for `SIGNED_IN` ([L823](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L823), [L938-L940](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L938-L940)). shadPS4 instead queues `SIGNED_OUT` from the user service login event and delivers it from `sceNpCheckCallback` ([DispatchPendingNpStateCallbacks](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L903-L962)); the reachability callback is not given an initial `UNAVAILABLE`, unlike that function ([L922-L961](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L922-L961)). A callback being delivered when another thread unregisters it is still called once. When and how often the console calls the state callbacks, and the errors, are not measured +- [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 +- [sceNpRegisterStateCallbackA and sceNpUnregisterStateCallbackA](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - never return the module's `0x80550002` for a failed [readiness check](https://github.com/mattias800/prosper/blob/6c09a34a092a00898fcead5f855200ca7c368cd2/prosper/src/hle/np/state_callbacks.cpp#L152-L153), whose input is not known, and are not measured on a PS5. An unregister id outside 1..8, compared unsigned so 0 and negative ids are included, returns `INVALID_ID` (0x80550015) and an empty slot `CALLBACK_NOT_REGISTERED`, as [prosper](https://github.com/mattias800/prosper/blob/6c09a34a092a00898fcead5f855200ca7c368cd2/prosper/src/hle/np/state_callbacks.cpp#L176-L188) reads `libSceNpManager.sprx` at RVA 0x15b10, without recording its firmware version; the same reading gives the registration rules above ([L162-L175](https://github.com/mattias800/prosper/blob/6c09a34a092a00898fcead5f855200ca7c368cd2/prosper/src/hle/np/state_callbacks.cpp#L162-L175)) - [sceNpRegisterGamePresenceCallback](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - accepts any callback, null included, and returns nothing; no game presence event is delivered because the NP model remains signed out. [Kyty](https://github.com/InoriRus/Kyty/blob/4733b7e1c91b10554a52007903d74dc76c39a230/source/emulator/src/Network.cpp#L1549-L1552) ignores the callback the same way, and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/np/np_manager.cpp#L1121-L1126) keeps the last one without calling it. Not measured on a PS5 - [sceNpIsPlusMember and sceNpSetContentRestriction](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the signatures and checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_manager.cpp): a null output or restriction is `INVALID_ARGUMENT`, a restriction size other than 0x18 is `INVALID_SIZE` (0x80550011), and a negative default age, more than 256 age restrictions or a count without an array is `INVALID_ARGUMENT`. Any user is reported as not a Plus member, and a valid restriction is accepted and not stored. Not measured on a PS5 @@ -32,5 +33,10 @@ - [sceNpAbortRequest](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - a request id that was never created or was deleted returns `SCE_NP_ERROR_REQUEST_NOT_FOUND` and a live request returns 0, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp) does in `sceNpAbortRequest`; shadPS4 there also moves the request into an `Aborted` state that its later requests read as `ABORTED`, and whether aborting on the console changes a live request's completion is not measured on a PS5 ([#9277](https://github.com/boykopovar/AnyPS5/issues/9277)), so nothing in this library reads an abort state and the id stays live - [sceNpDeleteRequest](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - a request id that was never created or was deleted returns `SCE_NP_ERROR_REQUEST_NOT_FOUND`, and a live request is removed and returns 0, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp) does in `sceNpDeleteRequest` ([#9277](https://github.com/boykopovar/AnyPS5/issues/9277)) +- [sceNpIdMapperCreateRequest, sceNpIdMapperDeleteRequest, sceNpIdMapperAbortRequest](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the in-library check is [measured on firmware 13.42 `libSceNpManager.sprx` by BunnyFl](https://github.com/boykopovar/AnyPS5/pull/10879#issuecomment-6397162725): delete and abort refuse a request id of 0 or less with `0x80553085`, and the rest goes to the NP service (create through `np::ipc::service_client::CreateRequest`, abort through `sceNpAsmClientAbortRequest`, delete through `sceNpAsmClientAbortRequest` and `sceNpAsmClientDeleteRequest`). The service's answers follow main's offline model of `sceNpCreateRequest`, not a console measurement: a counter of their own hands out ids from 1, abort leaves a live request live, delete frees it, and an id never created or already deleted returns `SCE_NP_ERROR_REQUEST_NOT_FOUND` +- [sceNpManagerIntCreateRequest, sceNpManagerIntDeleteRequest, sceNpManagerIntAbortRequest](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the in-library table is [measured on firmware 13.42 `libSceNpManager.sprx` by BunnyFl](https://github.com/boykopovar/AnyPS5/pull/10879#issuecomment-6397362281): 64 slots, so the 65th live request gives `0x80559D05` without asking the service; create returns a negative or zero service id as is and refuses one already live with `0x80559D03`; delete gives `0x80559D0A` for an id not in the table and frees a live one; abort has no local lookup. The ids and the abort answer follow main's offline model, not a console measurement: a counter of this family's own hands out ids from 1, and abort answers `SCE_NP_ERROR_REQUEST_NOT_FOUND` for an id the service does not hold. The measured in-use path (a request held by a running call is only marked deleted) is not reached, since no call holds a request +- [sceNpIdMapperNpIdToAccountId, sceNpIdMapperOnlineIdToAccountId](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the argument checks are [measured on firmware 13.42 `libSceNpManager.sprx` by BunnyFl](https://github.com/boykopovar/AnyPS5/pull/10879#issuecomment-6397162725): seven arguments (request id, user id, an input pointer, an output pointer, a 32-bit value, a byte and a seventh argument); a request id of 0 or less gives `0x80553085`, then user id -1, a null pointer or the value outside 1 to 1000 gives `0x80553083`, and the byte and the seventh argument are not checked. Past them the request goes to the NP service, whose answer follows main's offline model of the public twins, not a console measurement: `SCE_NP_ERROR_SIGNED_OUT`, writing nothing. The SDK names of the two codes are not known, so their constants are named after the checks +- [NP service client initialization](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - not modelled, so `0x80550002` is never returned: BunnyFl measured it from `sceNpManagerIntCreateRequest`, `sceNpManagerIntDeleteRequest` and `sceNpManagerIntAbortRequest` before the library's NP service client exists ([measured on firmware 13.42 `libSceNpManager.sprx` by BunnyFl](https://github.com/boykopovar/AnyPS5/pull/10879#issuecomment-6397362281)), and main keeps no initialized state for this library +- [sceNpIdMapperAccountIdToNpId, sceNpIdMapperAccountIdToOnlineId](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - still throw: this library gives no answer for an account id mapped to an online id or NpId (`sceNpManagerIntGetOnlineIdByAccountId` has no export here), and what a signed-out user's account id is depends on the offline account-id model of [#6990](https://github.com/boykopovar/AnyPS5/pull/6990) and [#7412](https://github.com/boykopovar/AnyPS5/pull/7412) - [sceNpPollAsync](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - a null result pointer returns `SCE_NP_ERROR_INVALID_ARGUMENT` before the id is read, a request id that was never created or was deleted returns `SCE_NP_ERROR_REQUEST_NOT_FOUND` without writing the caller's result, and a live request keeps its signed-out completion `0` with `SCE_NP_ERROR_SIGNED_OUT`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp) does in `sceNpPollAsync` ([#9277](https://github.com/boykopovar/AnyPS5/issues/9277)). The parts of that model beyond the validation are not implemented: shadPS4 keeps a per-request state machine decorated by `sceNpCheckNpAvailability` and neighbors and refuses `InvalidId` for a ready async request in wait/poll, whose completion publication, poll-versus-wait precedence and already-complete errors remain unattributable to the PS5 - [sceNpCreateAsyncRequest](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the parameter is not read ([NpCreateAsyncRequestParameter](../../../core/libs/SceTypes.hpp) is a 0x40-byte opaque blob, so shadPS4's nonnull check and `size == sizeof` reject ([81be949167d0060b08f26dc3e055a0b640d129c8](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp)) cannot be applied without inventing a layout) and no async kind or completion state is stored until the console's contract for them is evidenced ([#9277](https://github.com/boykopovar/AnyPS5/issues/9277)); `sceNpWaitAsync` (`jyi5p9XWUSs`) has no export here yet and stays with [PR #9282](https://github.com/boykopovar/AnyPS5/pull/9282) diff --git a/docs/dev/debt/libSceNpParty.md b/docs/dev/debt/libSceNpParty.md index e88c71d92d..8d6176415e 100644 --- a/docs/dev/debt/libSceNpParty.md +++ b/docs/dev/debt/libSceNpParty.md @@ -5,4 +5,5 @@ - [sceNpPartyGetState and sceNpPartyGetStateAsUserA](../../../core/libs/prx/libSceNpParty/Export.cpp) (libSceNpParty) - the 16-bit state, its `NOT_IN_PARTY` value 2 and the `INVALID_ARGUMENT` (`0x80552504`) error follow shadPS4's [np_party.h](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_party.h) and [np_error.h](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_error.h) (PS4); shadPS4 does not check for a null state, and whether the console needs an initialized party library first is not known. Not measured on a PS5 - [sceNpPartyGetId, sceNpPartyGetMembersA, sceNpPartyGetMemberInfoA, sceNpPartyGetMemberSessionInfo, sceNpPartyGetMemberVoiceInfo and sceNpPartySendBinaryMessage](../../../core/libs/prx/libSceNpParty/Export.cpp) (libSceNpParty) - the user is never in a party, so these return `NOT_IN_PARTY` (`0x80552506`, shadPS4's np_error.h) and write nothing, or `INVALID_ARGUMENT` for a null pointer; the signatures are from [Orbital's type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json). Like `sceNpPartyGetState` they do not require `sceNpPartyInitialize`, and the order of the null and party checks on the console is not known. Not measured on a PS5 - [sceNpPartyInitialize, sceNpPartyTerminate, sceNpPartyCheckCallback and sceNpPartyRegisterHandlerA](../../../core/libs/prx/libSceNpParty/Export.cpp) (libSceNpParty) - an initialized flag with `ALREADY_INITIALIZED` (`0x80552502`) and `NOT_INITIALIZED` (`0x80552503`) from shadPS4's np_error.h; the handlers are accepted but never called, since no party event can happen. Whether the console refuses a null initialize parameter, and which check comes first, is not known -- [sceNpPartyCreate, sceNpPartyCreateA, sceNpPartyGetMemberInfo, sceNpPartyGetMembers, sceNpPartyGetStateAsUser, sceNpPartyGetVoiceChatPriority, sceNpPartyJoin, sceNpPartyLeave, sceNpPartyRegisterHandler, sceNpPartyRegisterPrivateHandler, sceNpPartySetVoiceChatPriority, sceNpPartyShowInvitationList, sceNpPartyShowInvitationListA and sceNpPartyUnregisterPrivateHandler](../../../core/libs/prx/libSceNpParty/Export.cpp) (libSceNpParty) - Orbital has no prototypes for these and no credible ones 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 +- [sceNpPartyCreate, sceNpPartyCreateA, sceNpPartyGetStateAsUser, sceNpPartyGetVoiceChatPriority, sceNpPartyJoin, sceNpPartyLeave, sceNpPartyRegisterPrivateHandler, sceNpPartySetVoiceChatPriority, sceNpPartyShowInvitationList, sceNpPartyShowInvitationListA and sceNpPartyUnregisterPrivateHandler](../../../core/libs/prx/libSceNpParty/Export.cpp) (libSceNpParty) - Orbital has no prototypes for these and no credible ones 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 +- [sceNpPartyGetMembers, sceNpPartyGetMemberInfo and sceNpPartyRegisterHandler](../../../core/libs/prx/libSceNpParty/Export.cpp) (libSceNpParty) - the signatures are from [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/src/np/ps4_libscenpparty.pas) (PS4) and match their `A` variants, so they behave like them: no party, `NOT_IN_PARTY` from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_party.cpp), and handlers accepted but never called. How the console's non-`A` member structures differ from the `A` ones is not known, and nothing is written to them. Not measured on a PS5 diff --git a/docs/dev/debt/libSceNpSessionSignaling.md b/docs/dev/debt/libSceNpSessionSignaling.md index 33ed1372ba..e046ea466c 100644 --- a/docs/dev/debt/libSceNpSessionSignaling.md +++ b/docs/dev/debt/libSceNpSessionSignaling.md @@ -9,4 +9,4 @@ ### Functional -- [sceNpSessionSignalingGetGroupInfo, sceNpSessionSignalingGetGroupFromSessionId, sceNpSessionSignalingGetConnectionFromNetAddress, sceNpSessionSignalingGetConnectionFromNetAddress2, sceNpSessionSignalingGetConnectionFromPeerAddress, sceNpSessionSignalingGetConnectionFromPeerAddress2](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - no session ever activates here, so every lookup on a valid context ends at not-found (group 0x8055330A, connection 0x8055330C). The signatures and the checks before it are the PS5 12.00 module's, read from disassembly: 0x80553301 before initialization, 0x80553303 for a null pointer, then 0x80553308 for an unknown context +- [sceNpSessionSignalingGetGroupInfo, sceNpSessionSignalingGetGroupFromSessionId, sceNpSessionSignalingGetConnectionFromNetAddress, sceNpSessionSignalingGetConnectionFromNetAddress2, sceNpSessionSignalingGetConnectionFromPeerAddress, sceNpSessionSignalingGetConnectionFromPeerAddress2](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - no session ever activates here, so every lookup on a valid context ends at not-found (group 0x8055330A, connection 0x8055330C). The signatures and the checks before it are the PS5 13.60 module's, read from disassembly: 0x80553301 before initialization, 0x80553303 for a null pointer, then 0x80553308 for an unknown context diff --git a/docs/dev/debt/libSceNpSignaling.md b/docs/dev/debt/libSceNpSignaling.md index c0dba40607..1adbc6177b 100644 --- a/docs/dev/debt/libSceNpSignaling.md +++ b/docs/dev/debt/libSceNpSignaling.md @@ -1,9 +1,7 @@ # libSceNpSignaling technical debt -### Unknown function info - -- [sceNpSignalingDeactivateConnection](../../../core/libs/prx/libSceNpSignaling/Export.cpp) (libSceNpSignaling) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws - ### Functional - [libSceNpSignaling](../../../core/libs/prx/libSceNpSignaling/Export.cpp) (libSceNpSignaling) - offline only, not measured on a PS5: the checks, their order, context ids 1 to 8 and the NpId rule (`term` 0, 3 to 16 characters, the first an ASCII letter or digit, the rest ASCII letters, digits, `_` or `-`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_signaling/np_signaling.cpp#L118-L170) and its [state helpers](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_signaling/np_signaling_state.cpp#L120-L170). `sceNpSignalingInitialize` reserves no heap and starts no thread, so its arguments are not checked, and callbacks are never called. `sceNpSignalingActivateConnection` returns `SCE_NP_ERROR_SIGNED_OUT` after shadPS4's checks up to `OWN_NP_ID`, without its rate limit, and creates no connection, so the connection calls answer `CONN_NOT_FOUND` + +- [sceNpSignalingDeactivateConnection](../../../core/libs/prx/libSceNpSignaling/Export.cpp) (libSceNpSignaling) - not measured on a PS5: it has the same checks as `sceNpSignalingTerminateConnection` (not initialized, unknown context, then `CONN_NOT_FOUND`), after shadPS4's [call](https://github.com/shadps4-emu/shadPS4/blob/12a4564543ccec527f9723676d2180580d1d7a94/src/core/libraries/np/np_signaling/np_signaling.cpp#L378-L400). No connection is ever created, so none can be deactivated. diff --git a/docs/dev/debt/libSceNpSns.md b/docs/dev/debt/libSceNpSns.md index 9f14bbcc3a..29b552135c 100644 --- a/docs/dev/debt/libSceNpSns.md +++ b/docs/dev/debt/libSceNpSns.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceNpSnsFacebookAbortRequest, sceNpSnsFacebookCreateRequest, sceNpSnsFacebookDeleteRequest, sceNpSnsFacebookGetAccessToken, sceNpSnsIntAbortRequest, sceNpSnsIntCreateRequest, sceNpSnsIntDeleteRequest, sceNpSnsIntFbGetGameAccessToken, sceNpSnsIntFbGetGameAccessTokenAllowed, sceNpSnsIntFbGetSystemAccessToken, sceNpSnsIntFbGetTitleInfo, sceNpSnsIntLinkedStatus, sceNpSnsIntTwGetSystemAccessToken, sceNpSnsIntUnlink, sceNpSnsIntYtGetAccessToken, sceNpSnsIntYtRefreshMasterToken, sceNpSnsTwitchAbortRequest, sceNpSnsTwitchCreateRequest, sceNpSnsTwitchDeleteRequest, sceNpSnsTwitchGetAccessToken, sceNpSnsYouTubeAbortRequest, sceNpSnsYouTubeCreateRequest, sceNpSnsYouTubeDeleteRequest and sceNpSnsYouTubeGetAccessToken](../../../core/libs/prx/libSceNpSns/Export.cpp) (libSceNpSns) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpSns.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 +- [sceNpSnsFacebookAbortRequest, sceNpSnsIntAbortRequest, sceNpSnsIntCreateRequest, sceNpSnsIntDeleteRequest, sceNpSnsIntFbGetGameAccessToken, sceNpSnsIntFbGetGameAccessTokenAllowed, sceNpSnsIntFbGetSystemAccessToken, sceNpSnsIntFbGetTitleInfo, sceNpSnsIntLinkedStatus, sceNpSnsIntTwGetSystemAccessToken, sceNpSnsIntUnlink, sceNpSnsIntYtGetAccessToken, sceNpSnsIntYtRefreshMasterToken, sceNpSnsTwitchAbortRequest, sceNpSnsTwitchCreateRequest, sceNpSnsTwitchDeleteRequest, sceNpSnsTwitchGetAccessToken, sceNpSnsYouTubeAbortRequest, sceNpSnsYouTubeCreateRequest, sceNpSnsYouTubeDeleteRequest and sceNpSnsYouTubeGetAccessToken](../../../core/libs/prx/libSceNpSns/Export.cpp) (libSceNpSns) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpSns.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 ### Inventory exports declared elsewhere - sceNpSnsFacebookCreateRequest, sceNpSnsFacebookDeleteRequest and sceNpSnsFacebookGetAccessToken are also 3.20 exports of this module but are currently declared by #6977 in libSceNpSnsFacebookDialog, so they are omitted here to respect the duplicate-export rule; re-add if that PR's module attribution is rejected. + sceNpSnsFacebookCreateRequest, sceNpSnsFacebookDeleteRequest and sceNpSnsFacebookGetAccessToken are also 3.20 exports of this module but are declared in `libSceNpManager` ([`Export.cpp`](../../../core/libs/prx/libSceNpManager/Export.cpp)), so they are omitted here to respect the duplicate-export rule; re-add them here if that module attribution is corrected. diff --git a/docs/dev/debt/libSceNpTrophy.md b/docs/dev/debt/libSceNpTrophy.md index 82ca475ea5..b1b1fe76c5 100644 --- a/docs/dev/debt/libSceNpTrophy.md +++ b/docs/dev/debt/libSceNpTrophy.md @@ -3,6 +3,7 @@ ### Unknown function info - [sceNpTrophyRegisterContext, sceNpTrophyGetTrophyUnlockState, sceNpTrophyUnlockTrophy](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy, PS4) - the trophy definitions in `TROPCONF.ESFM` are encrypted, so the trophy count is the number of consecutive `TROPnnn.PNG` icons in the title's `/app0/sce_sys/trophy/trophyNN.trp`. 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 ### Functional diff --git a/docs/dev/debt/libSceNpTrophy2.md b/docs/dev/debt/libSceNpTrophy2.md index 189c214f1d..8552c9e0de 100644 --- a/docs/dev/debt/libSceNpTrophy2.md +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -16,5 +16,6 @@ - [Trophy queries](../../../core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp) - not confirmed on the console: `sceNpTrophy2GetTrophyInfo` of a trophy id that is not in the title's [trophy data](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp) (`sce_sys/trophy2/trophyNN.ucp`) returns `0x8055390A` (KytyPS5's `NP_TROPHY2_ERROR_INVALID_TROPHY_ID`) and writes nothing; a context not created or destroyed reports placeholders and no unlocks and returns success; hidden trophies report their names and descriptions before the unlock; `progress_percentage` weighs gold 6, silver 2 and bronze 1 and leaves the platinum out, as KytyPS5 does (the ratio of the published PSN trophy points). A group that is not in the data throws. Without the file, or when it is malformed, the queries report the old placeholders and `sceNpTrophy2GetTrophyInfoArray` lists only trophy 0. Icons are not read. Unlocks come from the [trophy store](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp). - [Trophy data limits](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): a `trophyNN.ucp` or `udsNN.ucp` larger than 256 MiB or listing more than 4096 files, and a JSON file in it that nests deeper than 64 levels or holds more than 1048576 values, are not used, and the title runs as without that file. The limits keep the memory the reader takes to the file and about 140 MiB of parsed JSON; the console's limits are not known. - [Text language](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): texts come from `tropmeta_.json` for the console language, then `en-US`, then the file's `defaultLanguage`. The tags for languages 0 to 29 are KytyPS5's; `uk-UA` for 30 (Ukrainian) is an assumption. +- [Trophy progress statistics](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): `max` (the largest value posted, starting from `initialValue`, or `minValue` without one), `int32` (0 to 2147483647) and a rule condition of an `eventName` and one `property` whose `comparator` is `==` and `value` an unsigned integer follow the rules a title ships for its progressive trophies, not a console measurement. Borderlands 4 (PPSA01494) feeds each one's `udsStatId` from `$._trophy_progress` of `_UpdateTrophyProgress`, through an `int32` `max` statistic and a rule on `$._trophy_id`. `latest` and the unsigned types follow KytyPS5. - [Platinum](../../../core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp): an unlock through any path unlocks the platinum that the trophy names in `platinumTrophyId` once every trophy naming that platinum is unlocked, as KytyPS5 does; trophies without `platinumTrophyId` are not required. With the title's trophy data loaded, a posted `_UnlockTrophy` of a trophy that is not in the data, of a platinum or of a progressive trophy is not stored and prints a `[trophy] ignored` line, as KytyPS5 refuses them, so a progressive trophy unlocks only through its statistic; without trophy data any id of 0 or more is stored, including ids the data would make the platinum, a progressive trophy or no trophy, and the platinum is not unlocked when the trophies it needs are. The console's behaviour is not known. - [Damaged records](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp): a line in `_trophy/_.txt` or `.stats.txt` that is not a new ` ` pair (a blank line, a carriage return, a repeated or negative id, a value that is not a number) makes `readRecord` throw a `std::runtime_error` that names the file and the line. Every call that reads that record then throws it instead of returning, which stops the title as any throwing export does, so the title gets no return value; main returned 0 from these calls. The failed read is not kept, so each later call reads the file again and throws again until the file is fixed or deleted. The unlock record is read by `sceNpTrophy2GetTrophyInfo`, `sceNpTrophy2GetTrophyInfoArray` and `sceNpTrophy2GetGameInfo` for a created context, by `sceNpTrophy2GetGroupInfo` and `sceNpTrophy2GetGroupInfoArray` when the title's trophy data is loaded, by a `sceNpUniversalDataSystemPostEvent` of an `_UnlockTrophy` that the trophy data does not refuse, and by a statistic change when the trophy data is loaded; the stat record by a `sceNpUniversalDataSystemPostEvent` of an event that a statistics rule copies and by the trophy queries of a progressive trophy with a statistic. What the console does with a damaged record is not known. diff --git a/docs/dev/debt/libSceNpTus.md b/docs/dev/debt/libSceNpTus.md index cb413bbf34..142eda2d11 100644 --- a/docs/dev/debt/libSceNpTus.md +++ b/docs/dev/debt/libSceNpTus.md @@ -16,7 +16,6 @@ - [sceNpTssGetDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusGetDataForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusGetDataForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusGetFriendsVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusGetFriendsVariableAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusGetFriendsVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature @@ -32,3 +31,4 @@ - [sceNpTusCreateNpTitleCtxA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - creates a title context without checking sign-in, up to 32 contexts (`TOO_MANY_OBJECTS` beyond); shadPS4 returns the `sceNpGetNpId` error instead. Requests are limited to 256, and the limits are not measured on a console. - [sceNpTssCreateNpTitleCtx and sceNpTssCreateNpTitleCtxA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - that TSS title contexts share the TUS context table and its 32-context limit is inferred from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/core/libraries/np/np_tus/np_tus.cpp#L146-L168), which redirects both to the TUS creators, and from TSS requests coming from `sceNpTusCreateRequest`; nothing is measured on a console. The `A` form is `sceNpTusCreateNpTitleCtxA`. The `NpId` form returns `INSUFFICIENT_ARGUMENT` for a null `npId` and `INVALID_ARGUMENT` for service label `0xFFFFFFFF` before the limit, as shadPS4's [sceNpTusCreateNpTitleCtx](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/core/libraries/np/np_tus/np_tus.cpp#L122-L143) does; the signatures are shadPS4's and fpPS4's, and Orbital's for the `A` form. - [sceNpTusPollAsync, sceNpTusWaitAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - there is no PSN, so every implemented `*Async` call whose arguments pass the checks starts a request that finishes at once with `SCE_NP_ERROR_SIGNED_OUT`, which poll and wait report and the blocking calls return; shadPS4 returns `SIGNED_OUT` from the `*Async` call itself when no shadNet client is signed in, and RPCS3 returns `INVALID_ONLINE_ID` while offline, marked there as probable behaviour. An abort stays set, so every later operation on the request, blocking ones included, finishes with `ABORTED`; in shadPS4 an abort only ends the operation in progress. Until an operation has started, poll returns 1 and wait returns `INVALID_ARGUMENT` as in shadPS4; RPCS3 [blocks until a result is set](https://github.com/RPCS3/rpcs3/blob/1a9dca3a3a84a18df866e3021918322bd295160a/rpcs3/Emu/NP/np_contexts.cpp#L40-L52). +- [sceNpTusGetData, sceNpTusGetDataVUser, sceNpTusGetMultiSlotVariable, sceNpTusGetMultiSlotVariableVUser, sceNpTusGetMultiUserVariableForCrossSave, sceNpTusSetData, sceNpTusSetDataVUser, sceNpTusSetMultiSlotVariable and sceNpTusSetMultiSlotVariableVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - block on their `*Async` forms with the same parameters, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/np/np_tus/np_tus.cpp) does, so they inherit those forms' unmeasured checks and the offline `SIGNED_OUT` result diff --git a/docs/dev/debt/libSceNpUniversalDataSystem.md b/docs/dev/debt/libSceNpUniversalDataSystem.md index 2e07cb9901..cbc943be5e 100644 --- a/docs/dev/debt/libSceNpUniversalDataSystem.md +++ b/docs/dev/debt/libSceNpUniversalDataSystem.md @@ -19,5 +19,5 @@ ### Functional -- [Trophy events](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): `sceNpUniversalDataSystemPostEvent` stores an `_UnlockTrophy` event with an int32 `_trophy_id` of 0 or more as an unlock for the user and service label of its context, as KytyPS5 does, except an id that [libSceNpTrophy2](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp) refuses when the title's trophy data is loaded (one not in the data, a platinum or a progressive trophy). `_UpdateTrophyProgress`, named only by a third-party unlocker, is logged once per trophy and not interpreted. A context need not be registered, and every post of an event that `sceNpUniversalDataSystemCreateEvent` returned and that is not yet destroyed returns success (a null or any other event pointer stops the game): KytyPS5's errors for an unknown or unregistered context and for a malformed unlock are not verified. The storage statistics stay zero. -- [Statistics](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): every other event goes through the title's rules in `sce_sys/uds/udsNN.ucp`, which [libSceNpTrophy2](../../../core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp) reads. Only statistics with `"aggregation":"latest"` and `dataType` `uint32` or `uint64` are kept, as in KytyPS5; no public source documents the other aggregations or types, so their definitions and rules are skipped. Only rules whose condition is an `eventName` alone and whose input is a `$.name` path (dotted for nested objects) are applied. A value is used when it is an integer of any width that is not negative and fits the statistic's type and `minValue`/`maxValue`; KytyPS5 requires the statistic's exact width, and whether the console converts, clamps or rejects other values is not known. Statistics are stored per user and service label, and every trophy's `unlockCondition` is compared after each event that sets one. +- [Trophy events](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): `sceNpUniversalDataSystemPostEvent` stores an `_UnlockTrophy` event with an int32 `_trophy_id` of 0 or more as an unlock for the user and service label of its context, as KytyPS5 does, except an id that [libSceNpTrophy2](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp) refuses when the title's trophy data is loaded (one not in the data, a platinum or a progressive trophy). `_UpdateTrophyProgress` goes through the title's statistic rules like any other event (below). A context need not be registered, and every post of an event that `sceNpUniversalDataSystemCreateEvent` returned and that is not yet destroyed returns success (a null or any other event pointer stops the game): KytyPS5's errors for an unknown or unregistered context and for a malformed unlock are not verified. The storage statistics stay zero. +- [Statistics](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): every other event goes through the title's rules in `sce_sys/uds/udsNN.ucp`, which [libSceNpTrophy2](../../../core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp) reads. Only `"aggregation":"latest"` and `"max"` statistics of `dataType` `uint32`, `uint64` or `int32` are kept; no public source documents the other aggregations (such as `count`, `collection` and `eachLatest`) or types, so their definitions and rules are skipped. Only rules whose input is a `$.name` path (dotted for nested objects) are applied. A value is used when it is an integer of any width that is not negative and fits the statistic's type and `minValue`/`maxValue`; KytyPS5 requires the statistic's exact width, and whether the console converts, clamps or rejects other values is not known. Statistics are stored per user and service label, and every trophy's `unlockCondition` is compared after each event that sets one. diff --git a/docs/dev/debt/libSceNpUtility.md b/docs/dev/debt/libSceNpUtility.md index 642d6ab2bd..95ec9ab84a 100644 --- a/docs/dev/debt/libSceNpUtility.md +++ b/docs/dev/debt/libSceNpUtility.md @@ -6,17 +6,18 @@ - [sceNpAppInfoIntCheckServiceAvailabilityA, sceNpAppInfoIntCheckServiceAvailabilityAll, sceNpAppInfoIntCheckServiceAvailabilityAllA, sceNpAppInfoIntCreateRequest, sceNpAppInfoIntDestroyRequest and sceNpAppInfoIntFinalize](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpAppInfoIntInitialize, sceNpAppLaunchLink2IntAbortRequest, sceNpAppLaunchLink2IntCreateRequest, sceNpAppLaunchLink2IntDestroyRequest, sceNpAppLaunchLink2IntFinalize and sceNpAppLaunchLink2IntGetCompatibleTitleIdList](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpAppLaunchLink2IntGetCompatibleTitleIdNum, sceNpAppLaunchLink2IntInitialize, sceNpAppLaunchLinkIntAbortRequest, sceNpAppLaunchLinkIntCreateRequest, sceNpAppLaunchLinkIntDestroyRequest and sceNpAppLaunchLinkIntFinalize](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. -- [sceNpAppLaunchLinkIntGetCompatibleTitleIdList, sceNpAppLaunchLinkIntGetCompatibleTitleIdNum, sceNpAppLaunchLinkIntInitialize, sceNpBandwidthTestShutdownWithDetailedInfo and sceNpLookupAbortRequest](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. -- [sceNpLookupCreateAsyncRequest, sceNpLookupCreateTitleCtxA, sceNpLookupNetAbortRequest and sceNpLookupNetCensorComment](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. +- [sceNpAppLaunchLinkIntGetCompatibleTitleIdList, sceNpAppLaunchLinkIntGetCompatibleTitleIdNum, sceNpAppLaunchLinkIntInitialize and sceNpBandwidthTestShutdownWithDetailedInfo](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. +- [sceNpLookupCreateTitleCtxA, sceNpLookupNetAbortRequest and sceNpLookupNetCensorComment](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpLookupNetConvertJidToNpId, sceNpLookupNetConvertNpIdToJid, sceNpLookupNetCreateRequest, sceNpLookupNetCreateTitleCtx, sceNpLookupNetDeleteRequest, sceNpLookupNetDeleteTitleCtx and sceNpLookupNetInit](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. -- [sceNpLookupNetInitWithFunctionPointer, sceNpLookupNetInitWithMemoryPool, sceNpLookupNetIsInit, sceNpLookupNetNpId, sceNpLookupNetSanitizeComment, sceNpLookupNetSetTimeout, sceNpLookupNetTerm and sceNpLookupPollAsync](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. -- [sceNpLookupSetTimeout, sceNpLookupWaitAsync, sceNpServiceChecker2IntAbortRequest, sceNpServiceChecker2IntCreateRequest, sceNpServiceChecker2IntDestroyRequest and sceNpServiceChecker2IntFinalize](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. +- [sceNpLookupNetInitWithFunctionPointer, sceNpLookupNetInitWithMemoryPool, sceNpLookupNetIsInit, sceNpLookupNetNpId, sceNpLookupNetSanitizeComment, sceNpLookupNetSetTimeout and sceNpLookupNetTerm](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. +- [sceNpServiceChecker2IntAbortRequest, sceNpServiceChecker2IntCreateRequest, sceNpServiceChecker2IntDestroyRequest and sceNpServiceChecker2IntFinalize](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpServiceChecker2IntGetServiceAvailability, sceNpServiceChecker2IntGetServiceAvailabilityA, sceNpServiceChecker2IntInitialize, sceNpServiceChecker2IntIsSetServiceType and sceNpServiceCheckerIntAbortRequest](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpServiceCheckerIntCreateRequest, sceNpServiceCheckerIntDestroyRequest, sceNpServiceCheckerIntFinalize, sceNpServiceCheckerIntGetAvailability, sceNpServiceCheckerIntGetAvailabilityList and sceNpServiceCheckerIntInitialize](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpServiceCheckerIntIsCached, sceNpTitleMetadataIntAbortRequest, sceNpTitleMetadataIntCreateRequest, sceNpTitleMetadataIntDeleteRequest, sceNpTitleMetadataIntGetInfo, sceNpTitleMetadataIntGetNpTitleId and sceNpUtilityInit](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpUtilityTerm and sceNpWordFilterCreateTitleCtxA](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. -- [sceNpWordFilterSetTimeout and sceNpWordFilterWaitAsync](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. ### Functional +- [sceNpBandwidthTestInitStart, sceNpBandwidthTestShutdownWithDetailedInfo](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - offline answers, not measured on a PS5: InitStart returns `SCE_NP_ERROR_SIGNED_OUT` as `sceNpBandwidthTestInitStartDownload` and `InitStartUpload` do, so no test is ever started, and ShutdownWithDetailedInfo returns `SCE_NP_ERROR_INVALID_ARGUMENT` and writes nothing, as `sceNpBandwidthTestShutdown` does. Neither reads an argument, so their parameterless declarations stand; their prototypes are still unknown - [NpLookup and NpWordFilter](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - offline only, not measured on a PS5: the argument checks and their order, the limit of 32 title contexts and 32 requests, the word filter id bases `0x01000001` and `0x02000001`, the 0x400-byte comment limit and single-use requests (`INVALID_TYPE`) follow shadPS4's [lookup](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_utility/np_utility.cpp#L41-L222) and [word filter](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_utility/np_utility.cpp#L705-L909) calls. `sceNpLookupNpId`, `sceNpWordFilterCensorComment` and `sceNpWordFilterSanitizeComment` then finish with `SCE_NP_ERROR_SIGNED_OUT` and write no NpId or comment; shadPS4's word filter accepts every comment as a stand-in for the server, so its result is not used +- [NpLookup and NpWordFilter waits and timeouts](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - IDs here carry no service: on the console, measured on firmware 13.42's module in an emulator (#10640), each ID holds its service in its high bits (Lookup requests `0x2xxxxxxx` and title contexts `0x4xxxxxxx`, word filter `0x02xxxxxx` and `0x04xxxxxx`), and `SetTimeout`, `AbortRequest`, `PollAsync` and `WaitAsync` find the object by those bits whichever service's call is made; here each call finds only its own service's IDs, which keep the ranges above. The timeouts are checked as measured and then have no effect, since no request reaches a server; the console stores them. A wait on a request deleted meanwhile returns `INVALID_ID`, unmeasured. The measured checks are followed: a null result first (`0x80550704`), a synchronous request (`0x80550307` for Lookup, `INVALID_ID` for the word filter), the timeout bounds and the 0x18-byte async parameter diff --git a/docs/dev/debt/libSceNpWebApi.md b/docs/dev/debt/libSceNpWebApi.md index 95fb314c74..af76876d59 100644 --- a/docs/dev/debt/libSceNpWebApi.md +++ b/docs/dev/debt/libSceNpWebApi.md @@ -3,4 +3,6 @@ ### Unknown function info - [sceNpWebApiInitialize, sceNpWebApiTerminate, sceNpWebApiCreateHandle, sceNpWebApiDeleteHandle, sceNpWebApiCreatePushEventFilter, sceNpWebApiDeletePushEventFilter](../../../core/libs/prx/libSceNpWebApi/Export.cpp) (libSceNpWebApi, PS4) - signatures, id ranges (library contexts 1 to 0x7fff, handles and filters below 0xf0000000) and errors (`LIB_CONTEXT_NOT_FOUND`, `HANDLE_NOT_FOUND`, `PUSH_EVENT_FILTER_NOT_FOUND`, `INVALID_ARGUMENT` for a missing filter parameter) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api/np_web_api_internal.cpp); they are not checked on a console. The libHttp context and pool size are kept but not used, since no request is ever sent. Limbo (CUSA01664) creates a context, a handle and filters when its game starts. -- [sceNpWebApiCreateServicePushEventFilter](../../../core/libs/prx/libSceNpWebApi/Export.cpp) (libSceNpWebApi, PS4) - a filter for a PSN service needs the signed-in user, and no user is signed in to PSN, so a valid call returns `SIGNED_IN_USER_NOT_FOUND` after the context and handle checks, as shadPS4 does with its network disabled. shadPS4 also rejects an invalid service label for titles built with SDK 2.00 or later; AnyPS5 has no compiled-SDK version for libraries yet, so such a call returns `SIGNED_IN_USER_NOT_FOUND` instead of `INVALID_ARGUMENT`. `sceNpWebApiDeleteServicePushEventFilter` therefore never finds a filter. +- [sceNpWebApiCreateServicePushEventFilter](../../../core/libs/prx/libSceNpWebApi/Export.cpp) (libSceNpWebApi, PS4) - a filter for a PSN service needs the signed-in user, and no user is signed in to PSN, so a valid call returns `SIGNED_IN_USER_NOT_FOUND` after the context and handle checks, as shadPS4 does with its network disabled. `sceNpWebApiDeleteServicePushEventFilter` therefore never finds a filter. +- [sceNpWebApiCreateContext, sceNpWebApiDeleteContext, sceNpWebApiCreateRequest, sceNpWebApiDeleteRequest, sceNpWebApiAbortRequest, sceNpWebApiSendRequest](../../../core/libs/prx/libSceNpWebApi/Export.cpp) (libSceNpWebApi, PS4) - the online id is not looked up: as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api/np_web_api_internal.cpp), a user context belongs to the initial user, so a library context holds at most one. With no user signed in to PSN, `sceNpWebApiSendRequest` returns `NOT_SIGNED_IN` unless the request is aborted. Id layouts and the delete, abort and not-found results match an offline PS4 Pro probe on firmware 10.71 (#9797); other checks follow shadPS4. That homebrew probe, with a non-retail SDK version, accepted method 99, so `INVALID_ARGUMENT` for a method after DELETE from SDK 2.50 is unverified. `sceNpWebApiTerminate` also deletes user contexts and requests; shadPS4 stops after the first user context. +- [sceNpWebApiRegisterPushEventCallback, sceNpWebApiUnregisterPushEventCallback, sceNpWebApiRegisterServicePushEventCallback, sceNpWebApiUnregisterServicePushEventCallback](../../../core/libs/prx/libSceNpWebApi/Export.cpp) (libSceNpWebApi, PS4) - registered callbacks are never called, since no push event arrives. A null function is rejected from SDK 1.00 and the filter must exist in the library context from SDK 2.50, as in shadPS4; this is not checked on a console. No service filter can be created, so from SDK 2.50 a service callback returns `SERVICE_PUSH_EVENT_FILTER_NOT_FOUND`. Callback ids come from the counter of handles and filters. diff --git a/docs/dev/debt/libSceNpWebApi2.md b/docs/dev/debt/libSceNpWebApi2.md index 7c73e05da4..923515fda6 100644 --- a/docs/dev/debt/libSceNpWebApi2.md +++ b/docs/dev/debt/libSceNpWebApi2.md @@ -3,6 +3,7 @@ ### Silent stubs - [sceNpWebApi2AddWebTraceTag](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - the `(request id, value)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api2/np_web_api2.cpp). A title imports it. It validates that the value pointer is not null (returning `INVALID_ARGUMENT` 0x80553402), accepts any request id, and stores nothing: no request reaches a service, as `sceNpWebApi2SendRequest` fails with `UNAVAILABLE` at once, so trace tags are not transmitted offline +- [sceNpWebApi2PushEventRegisterCallback and sceNpWebApi2PushEventRegisterPushContextCallback](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - they store no callback and return a new id for any user context and filter, so an unknown context or filter is not rejected and no callback is ever called: no push event reaches a title, as network push operations are unimplemented and `sceNpWebApi2PushEventCreatePushContext` fails with `UNAVAILABLE`. The callback's prototype is not identified. The `(user context id, filter id, callback, user argument)` signature comes from one disassembled title (PPSA21564 01.007.000), not from a console: it calls them at 0x2ba5f0 and 0x2ba9e0 respectively, both with the id `sceNpWebApi2CreateUserContext` returned, the id `sceNpWebApi2PushEventCreateFilter` returned just before, the address of one of its functions and a user argument, and treats a negative result as failure - [sceNpWebApi2SetRequestTimeout](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - the `(request id, timeout)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api2/np_web_api2.cpp). Pinball FX (PPSA08755) calls it while its main menu loads. It succeeds for any request id and stores nothing: no request reaches a service, as `sceNpWebApi2SendRequest` fails with `UNAVAILABLE` at once, so no timeout can expire. shadPS4 returns `LIB_CONTEXT_NOT_FOUND`, `USER_CONTEXT_NOT_FOUND` or `REQUEST_NOT_FOUND` for an id it does not know; this library keeps no context or request table (`sceNpWebApi2CreateRequest` hands out counter values, and `sceNpWebApi2AbortRequest`, `sceNpWebApi2AddHttpRequestHeader` and `sceNpWebApi2DeleteRequest` also accept any id), so those errors are not reported until the request functions track their ids together ### Unknown function info diff --git a/docs/dev/debt/libSceOpusCeltDec.md b/docs/dev/debt/libSceOpusCeltDec.md index 4ba1fc6999..731f5bbad3 100644 --- a/docs/dev/debt/libSceOpusCeltDec.md +++ b/docs/dev/debt/libSceOpusCeltDec.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceOpusCeltDecCreate, sceOpusCeltDecCtl, sceOpusCeltDecDecodeFloat, sceOpusCeltDecDecodeFloatWithPriority, sceOpusCeltDecDecodeWithPriority](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp) (libSceOpusCeltDec) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusCeltDec.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. +- [sceOpusCeltDecCreate, sceOpusCeltDecCtl](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp) (libSceOpusCeltDec) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusCeltDec.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. - [sceOpusCeltDecTerminate](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp): the non-null path forwards the AJM finalization result, but the null path returns without setting the return register. Its source return type cannot be established from this callee alone. The existing integer return is retained. ### Functional diff --git a/docs/dev/debt/libSceOpusDec.md b/docs/dev/debt/libSceOpusDec.md index d98513d94b..18d5334b3d 100644 --- a/docs/dev/debt/libSceOpusDec.md +++ b/docs/dev/debt/libSceOpusDec.md @@ -2,7 +2,8 @@ ### Unknown function info -- [sceOpusDecCancel, sceOpusDecCreate, sceOpusDecCtl, sceOpusDecDecodeFloat, sceOpusDecDecodeFloatWithPriority, sceOpusDecDecodeFloatWithPriorityAndTimeout, sceOpusDecDecodeWithPriority and sceOpusDecDecodeWithPriorityAndTimeout](../../../core/libs/prx/libSceOpusDec/Export.cpp) (libSceOpusDec) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusDec.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. +- [sceOpusDecCancel, sceOpusDecCreate and sceOpusDecCtl](../../../core/libs/prx/libSceOpusDec/Export.cpp) (libSceOpusDec) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusDec.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. +- [sceOpusDecDecodeWithPriorityAndTimeout and sceOpusDecDecodeFloatWithPriorityAndTimeout](../../../core/libs/prx/libSceOpusDec/Export.cpp) (libSceOpusDec) decode synchronously only for the infinite timeout 0xFFFFFFFF, the value sceOpusDecDecode passes, so the batch ID output is never written. libSceOpusDec.sprx (firmware 13.60, read from disassembly, not measured on a console) passes the timeout to its AJM batch wait; when the wait does not finish it stores the batch ID and returns 0x81740015. Whether a finite timeout finishes depends on the console's decode time, so any other timeout returns the argument errors and then throws. The priority (20 to 100) is validated but does not schedule anything ### Functional diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 000b382ce7..5c75033c4d 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -11,11 +11,12 @@ - [libScePad report and tracking exports](../../../core/libs/prx/libScePad/Export.cpp) - read, extended-read, tracker, UI, VR-controller and report argument registers are recovered. Feature reports use a byte report ID and 64-bit byte count, capped at 64. VR trigger effects consume a 56-byte record; the dual-controller call uses two handles and a mask with records at offsets 8 and 64. Remaining payload field layouts use incomplete declarations, including extended samples and kernel-owned extension reports. - [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `n3kSX62fgNo`) - the name is unknown, and the 0 returned after `scePadInit` is inferred, not measured. In the OS 10.01 system module the function takes no arguments, returns `0x80920005` before `scePadInit`, and otherwise sends a per-process value of 1 to ShellCore over Mbus, returning `0x809200ff` if that call fails and 0 if it succeeds, with or without an open handle. The #3177 probe measured `0x809200ff` in a process that is not a game; a game process is assumed to get 0. The ShellCore setting it changes is unknown and not modelled -- [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per port slot +- [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per port slot. One title (PPSA04354) disassembled for this entry calls it once (0xead26b), only when a change bit of its own is set, with a 32-bit value read through a pointer and 0 or 1 taken from the 0x4 bit of a flags value, and ignores the result. The call does not show the threshold or the default. - [PadQueueLegacyAudioHaptics, PadClearLegacyAudioHaptics](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - PadQueueLegacyAudioHaptics and PadClearLegacyAudioHaptics route legacy audio haptics only to port 0; multi-port queueing is not modelled - [scePadGetControllerInformation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - scePadGetControllerInformation always reports connectedCount = 1 and connected = true; behaviour across multiple open controller handles is not measured on hardware -- [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. Other port types throw +- [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 - [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. - [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 @@ -23,7 +24,7 @@ - [scePadIsDS4Connected, scePadIsBlasterConnected and scePadIsMoveConnected](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `bool (int handle)` signatures are recovered; the library reports one DualSense to every title (see Touchpad resolution below), so no DualShock 4, Blaster or Move controller is ever behind a handle and each call answers false for any handle. The console's answer for an invalid handle is not measured - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [scePadGetAccessibilityControllerState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad, which is not an Access controller, reports 0. The name hashes to NID `zBsgF0q8DIM`. In the OS 10.01 system module the `(int handle, uint32_t* state)` function returns `0x80920005` before `scePadInit`, then `INVALID_ARG` for a NULL `state` and `INVALID_HANDLE` for a bad handle, and otherwise asks the pad driver for a per-device value of the device behind the handle: 0 when no device is behind it, 1 when the driver has no answer, else the driver's value. What the values mean is unknown -- [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 `DUALSENSE = 2`. 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. +- [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. - [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 40a6135747..89f271596b 100644 --- a/docs/dev/debt/libScePlayGo.md +++ b/docs/dev/debt/libScePlayGo.md @@ -4,5 +4,5 @@ - [scePlayGoRequestNextChunk, scePlayGoSnapshot](../../../core/libs/prx/libScePlayGo/Export.cpp) (libScePlayGo) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePlayGo.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim - [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 every chunk id as installed, and `scePlayGoGetChunkId` lists ids 0 to 999. 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, and a package with more chunks would see ids from 1000 up as not listed. 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) - 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) - `scePlayGoGetSupportedOptionalChunk` and `scePlayGoGetOptionalChunk` report every bit set for `type` 0, 1 or 2, i.e. every optional chunk (language pack) 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. diff --git a/docs/dev/debt/libScePlayReady.md b/docs/dev/debt/libScePlayReady.md index f3b7a6a60c..c7677e9529 100644 --- a/docs/dev/debt/libScePlayReady.md +++ b/docs/dev/debt/libScePlayReady.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceBufferAlloc, sceBufferDup, sceBufferFree, sceBufferInit, sceBufferRealloc, scePlayReadyApiInitializeInitParams, scePlayReadyApiIsStarted, scePlayReadyApiStart, scePlayReadyApiStop, scePlayReadyApiVersion, scePlayReadyApiVersionString, scePlayReadyBufferFree, scePlayReadyBufferInit, scePlayReadyBufferMalloc, scePlayReadyBufferRealloc, scePlayReadyContentGetProperty, scePlayReadyContentSetProperty, scePlayReadyDomainCertEnumFini, scePlayReadyDomainCertEnumInit, scePlayReadyDomainCertEnumNext, scePlayReadyDomainCertFind, scePlayReadyDomainJoinGenerateChallenge, scePlayReadyDomainJoinProcessResponse, scePlayReadyDomainLeaveGenerateChallenge, scePlayReadyDomainLeaveProcessResponse, scePlayReadyEnvelopeClose, scePlayReadyEnvelopeGetSize, scePlayReadyEnvelopeInitializeRead, scePlayReadyEnvelopeOpen, scePlayReadyEnvelopeRead, scePlayReadyEnvelopeSeek, scePlayReadyFinalize, scePlayReadyFreeDecryptContext, scePlayReadyInitialize, scePlayReadyInitializeInitParams, scePlayReadyLicenseAcqGenerateAck, scePlayReadyLicenseAcqGenerateChallenge, scePlayReadyLicenseAcqGetAdditionalResponseData, scePlayReadyLicenseAcqProcessAckResponse, scePlayReadyLicenseAcqProcessResponse, scePlayReadyMemAlloc, scePlayReadyMemFree, scePlayReadyMemGetStats, 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 +- [sceBufferAlloc, sceBufferDup, sceBufferFree, sceBufferInit, sceBufferRealloc, scePlayReadyApiInitializeInitParams, scePlayReadyApiIsStarted, scePlayReadyApiStart, scePlayReadyApiStop, scePlayReadyApiVersion, scePlayReadyApiVersionString, scePlayReadyBufferFree, scePlayReadyBufferInit, scePlayReadyBufferMalloc, scePlayReadyBufferRealloc, scePlayReadyContentGetProperty, scePlayReadyContentSetProperty, scePlayReadyDomainCertEnumFini, scePlayReadyDomainCertEnumInit, scePlayReadyDomainCertEnumNext, scePlayReadyDomainCertFind, scePlayReadyDomainJoinGenerateChallenge, scePlayReadyDomainJoinProcessResponse, scePlayReadyDomainLeaveGenerateChallenge, scePlayReadyDomainLeaveProcessResponse, scePlayReadyEnvelopeClose, scePlayReadyEnvelopeGetSize, scePlayReadyEnvelopeInitializeRead, scePlayReadyEnvelopeOpen, scePlayReadyEnvelopeRead, scePlayReadyEnvelopeSeek, scePlayReadyFinalize, scePlayReadyFreeDecryptContext, scePlayReadyInitialize, scePlayReadyInitializeInitParams, scePlayReadyLicenseAcqGenerateAck, 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 diff --git a/docs/dev/debt/libScePngDec.md b/docs/dev/debt/libScePngDec.md index 9449f5a2e9..260202e2bf 100644 --- a/docs/dev/debt/libScePngDec.md +++ b/docs/dev/debt/libScePngDec.md @@ -3,7 +3,7 @@ ### Functional - [libScePngDec](../../../core/libs/prx/libScePngDec/Export.cpp): the supplied module confirms the work-memory size formula, 16-byte handle alignment, the first 24 bytes of its context, and Create/QueryMemorySize validation order. The host now exposes these values but still allocates decoder working storage on the host instead of using the guest work region. -- [scePngDecDecode](../../../core/libs/prx/libScePngDec/Export.cpp): 16-bit output and the original's accepted pixel format 132 remain unsupported and throw. Malformed-image diagnostics and decoder scratch-memory behavior are not fully matched to the supplied module. +- [scePngDecDecode](../../../core/libs/prx/libScePngDec/Export.cpp): the original's accepted pixel format 132 remains unsupported and throws. The input bit-depth limit and 16-to-8-bit conversion follow [Prosper's PS5 3.20 module analysis](https://github.com/mattias800/prosper/blob/550886e3677e4a2ef81ad1603bf313f6a9696de2/prosper/src/hle/util/hle_pngdec.cpp#L412-L414), and the large-dimension image-info requirement and its validation order follow [Prosper's PS5 3.20 module analysis](https://github.com/mattias800/prosper/blob/550886e3677e4a2ef81ad1603bf313f6a9696de2/prosper/src/hle/util/hle_pngdec.cpp#L385-L396), not console measurements here. Malformed-image diagnostics and decoder scratch-memory behavior are not fully matched to the supplied module. ### Unknown function info diff --git a/docs/dev/debt/libScePosixForWebKit.md b/docs/dev/debt/libScePosixForWebKit.md index efe3b429e8..bf4a170fa8 100644 --- a/docs/dev/debt/libScePosixForWebKit.md +++ b/docs/dev/debt/libScePosixForWebKit.md @@ -4,13 +4,14 @@ - [gethostbyname](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp) (libScePosixForWebKit) - numeric IPv4 forms, trailing whitespace and 64-bit accumulation follow FreeBSD 11.4 [inet_aton](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/inet/inet_addr.c); clearing the resolver error on numeric success follows [fakeaddr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/net/gethostnamadr.c). Exact PS5 behavior is unverified; hostname lookup still uses the host resolver - [clock_gettime_np, getenv_np, gethostbyname_r (`scePosixForWebKit_gethostbyname_r`), mmap_np, msetname, pthread_setschedparam_np, setenv_np, sysinfo (`scePosixForWebKit_sysinfo`)](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) - names/NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePosixForWebKit.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); credible prototypes are unknown. These exports use renamed helpers; defining `gethostbyname_r` would interpose on the host function this library calls itself. All are parameterless import stubs, not ABI claims; callers must not use them until signatures are verified. -- [__wrap_fchown, __wrap_link, __wrap_readlink, __wrap_statvfs, __wrap_symlink](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) - parameterless throwing stubs. Under the `--wrap` convention assumed for the forwarding wrappers (see Functional) each would take the POSIX signature of the function it wraps, but the targets are in libkernel_sys (`fchown`, `link`, `readlink`, `symlink`) and libSceLibcInternal (`statvfs`), which no prx links; libkernel_sys's `fchown`, `readlink` and `symlink` also still throw for some arguments ### Functional - [strcasestr](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) - console locale behavior is unmeasured. It follows FreeBSD 11.4's [strcasestr.c](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/string/strcasestr.c) in the guest's supported C locale; other guest locales are not implemented. - [mkstemp and isatty](../../../core/libs/prx/libScePosixForWebKit/Filesystem.cpp) (libScePosixForWebKit) follow FreeBSD's [temporary-file API](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/stdio/mktemp.c) and [terminal check](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/gen/isatty.c), without console verification. `mkstemp` replaces all trailing `X` characters, accepts a fixed name, and creates files through the guest path resolver with exclusive read/write access and mode 0600. Windows file permissions follow the existing kernel file implementation. `isatty` tests Windows console handles rather than all character devices; files, pipes and guest sockets return `ENOTTY`, closed descriptors return `EBADF`. +- [__wrap_fchown, __wrap_link, __wrap_readlink, __wrap_statvfs and __wrap_symlink](../../../core/libs/prx/libScePosixForWebKit/Wrap.cpp) (libScePosixForWebKit) - whether the console's wrappers add sandbox checks or path rewriting for WebKit is unknown. Each takes the signature of the function it is named after, as GNU ld's `--wrap` does, and calls it: libkernel_sys's `fchown`, `link`, `readlink` and `symlink`, and libSceLibcInternal's `statvfs`, with their gaps - [arc4random](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) returns four bytes of host OS entropy (`getentropy`, `rand_s` on Windows), as `arc4random_buf` does, uniform over the 32-bit range like FreeBSD 11.4's RC4 keystream in [arc4random.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/arc4random.c). The console's generator and its reseeding are unknown; `arc4random_uniform`, `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent +- [pthread_setname_np](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) stores the guest thread name in libkernel's thread object, so `pthread_getname_np` returns it, and follows libkernel's `pthread_rename_np`: `EINVAL` (22) for a null thread or name, `ENAMETOOLONG` (63) from 32 bytes on, `0` otherwise, with the name truncated to 31 bytes when read back. It does not apply the Windows thread description or the job affinity that `scePthreadRename` sets, because those helpers are private to libkernel; the console's exact error numbers for this module are unverified - [fnmatch](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) follows FreeBSD 11 [fnmatch.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/gen/fnmatch.c) in the C locale: each byte is one character, `FNM_CASEFOLD` folds ASCII letters only, and bracket ranges compare byte values. `[:class:]` names are not character classes: as in FreeBSD 11 through 14, `[[:digit:]]` is the set of `[`, `:`, `d`, `i`, `g`, `t` followed by a literal `]` (classes were added to `fnmatch.c` after 14). Multibyte characters and locale collation are not modelled. A null pattern or string throws - [getservbyname](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp) (libScePosixForWebKit) searches a built-in copy of 37 lines of FreeBSD 11.4's [etc/services](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/etc/services) (ftp-data, ftp, ssh, telnet, smtp, domain, tftp, http, pop3, ntp, imap, https, rtsp, submission, imaps, pop3s with their aliases and protocols, in file order) the way [getservent.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/net/getservent.c) searches the file: case-sensitive name or alias, a null protocol takes the first match, the port in network byte order, the result in per-thread storage. The console's `/etc/services` is unknown, so any other service returns null without setting `errno`, as for a name the file lacks. `getaddrinfo` still resolves service names through the host's service database - [__wrap_access, __wrap_clock_gettime, __wrap_getcwd, __wrap_getenv, __wrap_lstat and __wrap_mmap](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) assume, from the export names alone, the GNU ld [`--wrap`](https://sourceware.org/binutils/docs/ld/Options.html) convention (a reference to `` resolves to `__wrap_`): each takes its target's POSIX signature and forwards to libc's (`access`, `getcwd`, `getenv`) or libkernel's (`clock_gettime`, `lstat`, `mmap`) implementation, which sets `errno`. No PS5 module or WebKit call site was read to confirm that WebKit imports `__wrap_` in place of `` or that the console wrappers only forward. Any sandbox or path handling the console module adds is not modelled, and how the wrappers relate to `getenv_np`, `setenv_np`, `mmap_np` and `clock_gettime_np` (such as a separate environment that `setenv_np` fills) is unknown diff --git a/docs/dev/debt/libScePsml_debug.md b/docs/dev/debt/libScePsml_debug.md index f77c32d0b1..9a520c2282 100644 --- a/docs/dev/debt/libScePsml_debug.md +++ b/docs/dev/debt/libScePsml_debug.md @@ -2,13 +2,13 @@ ### Unknown function info -- [scePsmlMfsrGetSharedResourcesInitRequirement, scePsmlMfsrCreateSharedResources, scePsmlMfsrReleaseSharedResources, scePsmlMfsrGetContextBufferRequirement800M3_2, scePsmlMfsrCreateContext800M3_2, scePsmlMfsrReleaseContext](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - only resource type 0 is supported. The context requirement reports four 2 MiB blocks regardless of its parameters, while the system calculation depends on the context configuration. The descriptor pointer at 0x28 is not read. Validation throws for null pointers, short block lists and foreign handles instead of reproducing the system error and fault behaviour. -- [scePsmlMfsr2GetDispatchPacketsSizeInDwords](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - writes a 32-bit size through the second argument; the unused first argument still needs caller verification. -- [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; PPSA26344 imports but does not call the first two and at `0x87EFA` passes `(rdi, rsi=&stack, rdx)` as three pointers to `GetDispatchMfsrPacket1100`; before `scePsmlMfsrInit` they return 0x8A810001 (not initialized) and after it they throw. `scePsmlMfsrCreateContext1300` and `scePsmlMfsrGetDispatchMfsrPacket900`, `1000` and `1300` take the same shapes (context slot and parameter; context, command buffer and parameter) and behave the same way. PPSA08330 and PPSA03016 pass `GetDispatchMfsrPacket900`/`1000` the context handle, a `{cursor, size in dwords}` command buffer and the dispatch parameters, and advance their cursor by the size `scePsmlMfsrGetDispatchMfsrPacketSizeInDwords` returned. Whether the 1100 and 1300 versions take these arguments, and the console's error, are unverified +- [scePsmlMfsrGetSharedResourcesInitRequirement, scePsmlMfsrCreateSharedResources, scePsmlMfsrReleaseSharedResources, scePsmlMfsrGetContextBufferRequirement800M3_2, scePsmlMfsrCreateContext800M3_2, scePsmlMfsrReleaseContext](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - verified from the firmware disassembly: resource type 0 is always allowed, types 1 and 2 need a console feature flag set during initialization (disallowed returns 0x8A81000D), and only one shared-resources object may exist at a time (a second create returns 0x8A810015). The context requirement reports four 2 MiB blocks regardless of its parameters, while the system calculation depends on the context configuration. The descriptor pointer at 0x28 is not read. Validation throws for null pointers, short block lists and foreign handles instead of reproducing the system error and fault behaviour. +- [scePsmlMfsr2Init and the other MFSR2 calls](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - only a console without PSSR is modelled. In firmware 13.60 `libScePsml.sprx` (read from disassembly, not measured on a console), `scePsmlMfsr2Init` returns 0x8A8A0012 when `sceKernelIsTrinityMode` returns 0, and until `Init` succeeds every other MFSR2 call returns 0x8A8A0001 without touching its arguments. AnyPS5 reports 0, so `Init` throws only if that changes: the initialization after the check, the shared resource and context requirements, and the 0x800-dword size that `GetDispatchPacketsSizeInDwords` writes through its second argument are not implemented +- [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; PPSA26344 imports but does not call the first two and at `0x87EFA` passes `(rdi, rsi=&stack, rdx)` as three pointers to `GetDispatchMfsrPacket1100`; they return 0x8A810001 (not initialized) and the `NotImplemented` arm is unreachable while `scePsmlMfsrInit` cannot succeed on the modelled console. `scePsmlMfsrCreateContext1300` and `scePsmlMfsrGetDispatchMfsrPacket900`, `1000` and `1300` take the same shapes (context slot and parameter; context, command buffer and parameter). PPSA08330 and PPSA03016 advance a command-buffer cursor by the `scePsmlMfsrGetDispatchMfsrPacketSizeInDwords` size. Firmware 13.60 copies a 0x158-byte template per variant into a shared writer that rotates eight ring slots and emits AGC packets; the 1100/1300 arguments and console errors are unverified - [scePsmlMfsrGetDispatchMfsrPacket1000](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA25344. libScePsml and libScePsml_debug are built from the same source - [scePsmlMfsrCreateContext1300, scePsmlMfsrGetDispatchMfsrPacket1300](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA28416 next to `scePsmlMfsrGetContextBufferRequirement1100`. The names are not in the aerolib NID list; they hash to the imported NIDs `kLF7WtQ1-Dg` and `DDVUyPyYHTA` (`tools/nid_names.py`) ### Functional -- [scePsmlMfsrGetDispatchMfsrPacketSizeInDwords, scePsmlMfsrGetMipmapBias, scePsmlMfsrSelectConfig, scePsmlMfsrRequestCapture, scePsmlMfsrIsCaptureInProgress](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - semantics from the APR-EMU `fakelib/libScePsml.sprx`. Guards: `0x8A810001` uninitialized, `0x8A810005` null/foreign handle (fakelib checks an engine id `0xA9C4`/`0x9231`; our map check is the equivalent), `0x8A810009` null output. `GetDispatchMfsrPacketSizeInDwords` writes `0x600`; `GetMipmapBias(context, bias, dimension)` writes `log2f(dimension / context->inputWidth) - 1.0f`, width recorded at `CreateContext800M3_2` from the parameter pair at 0x18 or `descriptor0 + 0x20` for callers that pass a descriptor pointer there (indirection inferred; zero width returns `0x8A810005`). `SelectConfig`, `RequestCapture`, `IsCaptureInProgress` validate then return 0. The `GetDispatchMfsrPacket*` calls still throw: the 0x158-byte template's packet routine is not understood -- [scePsmlMfsrInit](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - succeeds and only sets the initialized state; the shared resources and contexts are bookkeeping objects that use none of the guest blocks. Whether Sony's library fails on hardware without PSSR is unknown. PPSA08330 calls the setup unconditionally and only uses a context when its cached `sceKernelIsTrinityMode` result is set (0x3c643d, 0x567f97; otherwise it stores a -0.25 mip bias itself at 0x567fe2), which AnyPS5 reports as 0 +- [scePsmlMfsrGetDispatchMfsrPacketSizeInDwords, scePsmlMfsrGetMipmapBias, scePsmlMfsrSelectConfig, scePsmlMfsrRequestCapture, scePsmlMfsrIsCaptureInProgress](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - semantics verified against the real Sony `libScePsml.sprx` in PPSA20397's `fakelib`. Guards: `0x8A810001` uninitialized, `0x8A810005` null/foreign handle (the binary checks an engine id `0xA9C4`/`0x9231`; our map check is the equivalent), `0x8A810009` null output. `GetDispatchMfsrPacketSizeInDwords` writes `0x600`; `GetMipmapBias(context, bias, dimension)` writes `log2f(dimension / context->inputWidth) - 1.0f` (the binary reads the recorded width at context `+0x1920`), width recorded at `CreateContext800M3_2` from the parameter pair at 0x18 or `descriptor0 + 0x20` (zero width returns `0x8A810005`). `SelectConfig`, `IsCaptureInProgress` validate then return 0; `RequestCapture` validates, logs a one-shot message and returns 0. +- [scePsmlMfsrInit](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - mirrors the console library (verified from the firmware disassembly): it logs the release banner, then returns 0x8A810012 when `sceKernelIsTrinityMode` returns 0 — ahead of the initialized-state check, so repeated calls also return 0x8A810012 — and every later MFSR call returns 0x8A810001. On a Trinity console the real routine sets its initialized flag (a second call then returns 0x8A810002) and builds the contexts and shared resources as bookkeeping objects; AnyPS5 reports 0 for `sceKernelIsTrinityMode`, so `Init` throws if that ever changes instead of fabricating the real init path. PPSA08330 calls the setup unconditionally and only uses a context when its cached `sceKernelIsTrinityMode` result is set (0x3c643d, 0x567f97; otherwise it stores a -0.25 mip bias itself at 0x567fe2) diff --git a/docs/dev/debt/libSceRegMgr.md b/docs/dev/debt/libSceRegMgr.md index 24cd287ce7..939ecae49d 100644 --- a/docs/dev/debt/libSceRegMgr.md +++ b/docs/dev/debt/libSceRegMgr.md @@ -1,5 +1,10 @@ # libSceRegMgr technical debt +### Functional + +- [Registry store](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - entries are queried through an in-memory process-local store without persistence. Callable prototypes for `sceRegMgrGetInt`, `sceRegMgrGetStr`, and `sceRegMgrGetBin` derive from [regmgr.h](https://github.com/ps5-payload-dev/regdump/blob/883524cc48c8497267ba7fc94ff0ac58880c4227/regmgr.h#L3713-L3729). Implemented getters return error codes for invalid parameters (`0x80820001`), missing keys (`0x80820002`), buffer sizing (`0x80820004`), and type mismatches (`0x80820006`) as unverified host implementation assumptions, as the cited regmgr.h does not establish error codes or validation behavior. Setters (`sceRegMgrSetInt`, `sceRegMgrSetStr`, `sceRegMgrSetBin`, `sceRegMgrSetbin`), reset (`sceRegMgrResetVal`), and zero-length getter paths remain unimplemented throwing `NotImplemented_nid_no_patch` until callable contracts and error codes are supported by evidence. +- [Unverified registry APIs](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - export names and NIDs for initial-value getters, version queries, region queries and conversions, error checks, control/backup routines, and `sceRegMgrNonSys*` variants are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/cf6eede3f6588d779664e01874638f09484324b7/libSceRegMgr.c) and [NID database](https://github.com/zecoxao/sce_symbols/blob/9620e03b2964dbf0cfc1cb143ab24a65125091de/aerolib.csv), but their callable contracts and runtime behavior remain unverified and retain `NotImplemented_nid_no_patch`. + ### Unknown function info - [sceRegMgrCheckError](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md index 12645457b4..f2efe68a28 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -9,7 +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, the behaviour main had before; those are not measured on a PS5. -- [sceRtcGetDosTime, sceRtcSetDosTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date +- [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. - [sceRtcGetCurrentRawNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console reads clock 0x13 and returns its time plus the Unix epoch in ticks, or `0x80B50001` when the clock is unsupported (errno 5). The raw network clock is not modelled; this returns the host clock. Read from the disassembly of libSceRtc.sprx on firmware 13.60, not measured on a console - [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 diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index 5dd2782a8b..b022421794 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -16,9 +16,9 @@ ### Functional - [sceSaveDataDirNameSearch, sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - only the running title's saves are kept, so a search with another title's `title_id` reports zero hits, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/ec334904604ba41f5d4753f4a2498ec82540e5d6/src/core/libraries/save_data/savedata.cpp#L791-L802) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L592-L596) do when that title has no save directory; what a console returns for another title's saves is not verified. The running title's ID is the `titleId` in its `param.json` -- [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) runs the same search as `sceSaveDataDirNameSearch` with the same condition and result structures, as [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/SaveData/SaveDataExports.cs) does, its only source; what distinguishes it on the console is unknown. +- [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) runs the same search as `sceSaveDataDirNameSearch` with the same condition and result structures, as [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/SaveData/SaveDataExports.cs) does, its only source; what distinguishes it on the console is unknown. One title (PPSA35669, in a plugin) disassembled for this entry has one call site (0x2eda71); it clears 56 bytes for the result structure first (the size of the structure used here), puts a value returned by a call with the argument 0x8000 at offset 8 and 0x400 at offset 0x10, and afterwards branches on a negative result and reads the 32-bit field at offset 0, skipping its loop when it is 0. That agrees with the result structure of `sceSaveDataDirNameSearch`. The condition structure was not read. - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a failed mount (BUSY, EXISTS, NOT_FOUND, MOUNT_FULL) leaves the result as it was, as shadPS4 does; not checked on hardware. Animal Well (PPSA22520) depends on it: it checks for its save with a mount it never unmounts, then saves with a second mount of the same directory, ignores its BUSY and keeps using the first mount point for `sceSaveDataPrepare`, its file and `sceSaveDataUmount2` - [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, sceSaveDataGetEventResult, sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataLoadIcon, 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; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup +- [sceSaveDataBackup, sceSaveDataDelete, sceSaveDataDirNameSearch, sceSaveDataGetEventResult, sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataLoadIcon, sceSaveDataMount3, sceSaveDataSaveIcon, sceSaveDataSetParam, sceSaveDataUmount2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions, an implementation assumption matching guest error expectations not checked on hardware; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup - [sceSaveDataSyncSaveDataMemory](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - validates library initialization and parameters; since in-memory saves flush synchronously on write, sync confirms existence and readiness of the memory blob diff --git a/docs/dev/debt/libSceSdma.md b/docs/dev/debt/libSceSdma.md index 78b6855431..bef40fd068 100644 --- a/docs/dev/debt/libSceSdma.md +++ b/docs/dev/debt/libSceSdma.md @@ -2,4 +2,4 @@ ### Unknown function info -- [libSceSdma pending exports](../../../core/libs/prx/libSceSdma/Export.cpp) - 26 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSdma.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`. +- [libSceSdma pending exports](../../../core/libs/prx/libSceSdma/Export.cpp) - 21 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSdma.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`. diff --git a/docs/dev/debt/libSceSharePlay.md b/docs/dev/debt/libSceSharePlay.md index f7255df0d8..915f67c4c2 100644 --- a/docs/dev/debt/libSceSharePlay.md +++ b/docs/dev/debt/libSceSharePlay.md @@ -2,11 +2,12 @@ ### Unknown function info -- [sceSharePlayCrashDaemon, sceSharePlayGetCurrentConnectionInfo, sceSharePlayGetCurrentInfo, sceSharePlayGetEvent, sceSharePlayNotifyDialogOpen, sceSharePlayNotifyForceCloseForCdlg, sceSharePlayResumeScreenForCdlg, sceSharePlayServerLock](../../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - the list is the intersection of the export table of the console's [libSceSharePlay](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSharePlay.c) (function symbols only), the pinned [aerolib names](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) and the functions not yet defined anywhere in the repo. No credible prototypes were found; they are declared without parameters solely to resolve known imports, not as an ABI claim, and callers must not call them until signatures are verified. +- [sceSharePlayCrashDaemon, sceSharePlayGetCurrentInfo, sceSharePlayGetEvent, sceSharePlayNotifyDialogOpen, sceSharePlayNotifyForceCloseForCdlg, sceSharePlayResumeScreenForCdlg, sceSharePlayServerLock](../../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - the list is the intersection of the export table of the console's [libSceSharePlay](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSharePlay.c) (function symbols only), the pinned [aerolib names](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) and the functions not yet defined anywhere in the repo. No credible prototypes were found; they are declared without parameters solely to resolve known imports, not as an ABI claim, and callers must not call them until signatures are verified. - [sceSharePlayServerUnLock, sceSharePlaySetMode, sceSharePlaySetProhibition, sceSharePlaySetProhibitionModeWithAppId, sceSharePlayStartStandby, sceSharePlayStartStreaming, sceSharePlayStopStandby, sceSharePlayStopStreaming](../../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - the list is the intersection of the export table of the console's [libSceSharePlay](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSharePlay.c) (function symbols only), the pinned [aerolib names](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) and the functions not yet defined anywhere in the repo. No credible prototypes were found; they are declared without parameters solely to resolve known imports, not as an ABI claim, and callers must not call them until signatures are verified. ### Functional - [sceSharePlayInitialize and sceSharePlayTerminate](../../../core/libs/prx/libSceSharePlay/Export.cpp) - native heap validation and initialization state are recovered: a null heap ignores the supplied size, a supplied heap requires at least 0x1800 bytes, repeated initialization returns 0x810E0003 and termination before initialization returns 0x810E0004. The host does not allocate the native internal heap, connect to the Share Play service or reproduce its failures. - [sceSharePlayGetCurrentConnectionInfoA](../../../core/libs/prx/libSceSharePlay/Export.cpp) - the native module confirms a 72-byte result, initial zero fields and both user IDs -1. Initialization is checked before the output pointer. The host retains this initial disconnected state; native event draining, cached updates and service reconnection are unimplemented. +- [sceSharePlayGetCurrentConnectionInfo](../../../core/libs/prx/libSceSharePlay/Export.cpp) - the 56-byte layout without account IDs is from [shadPS4's shareplay.h](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/share_play/shareplay.h); the checks and values copy `sceSharePlayGetCurrentConnectionInfoA`, while shadPS4 leaves both user IDs 0 and checks nothing. Not measured on a console. - [sceSharePlayProhibitController](../../../core/libs/prx/libSceSharePlay/Export.cpp) - the argument is one byte, forwarded unchanged to the service. The host checks initialization, accepts 0/1 without a visitor controller, and returns 0x810E0001 (SCE_SHARE_PLAY_ERROR_INVALID_ARGS) for other values; dynamic service communication and controller state remain unimplemented. diff --git a/docs/dev/debt/libSceSpSysCallWrapper.md b/docs/dev/debt/libSceSpSysCallWrapper.md deleted file mode 100644 index ea6d5fbf4e..0000000000 --- a/docs/dev/debt/libSceSpSysCallWrapper.md +++ /dev/null @@ -1,5 +0,0 @@ -# libSceSpSysCallWrapper technical debt - -### Unknown function info - -- [sceSpGetPid, sceSpKernelGettimeofday, sceSpKernelNanosleep, sceSpNetConnect, sceSpNetEpollAbort, sceSpNetEpollControl, sceSpNetEpollCreate, sceSpNetEpollDestroy, sceSpNetEpollWait, sceSpNetErrnoLoc, sceSpNetPoolCreate, sceSpNetPoolDestroy, sceSpNetRecv, sceSpNetResolverAbort, sceSpNetResolverCreate, sceSpNetResolverDestroy, sceSpNetResolverGetError, sceSpNetResolverStartNtoa, sceSpNetSend, sceSpNetSetsockopt, sceSpNetSocket, sceSpNetSocketClose, sceSpPthreadAttrDestroy, sceSpPthreadAttrInit, sceSpPthreadAttrSetinheritsched, sceSpPthreadAttrSetschedparam, sceSpPthreadAttrSetstacksize, sceSpPthreadCondDestroy, sceSpPthreadCondInit, sceSpPthreadCondSignal, sceSpPthreadCondWait, sceSpPthreadCondattrDestroy, sceSpPthreadCondattrInit, sceSpPthreadCreate, sceSpPthreadDetach, sceSpPthreadExit, sceSpPthreadJoin, sceSpPthreadMutexDestroy, sceSpPthreadMutexInit, sceSpPthreadMutexLock, sceSpPthreadMutexUnlock, sceSpPthreadMutexattrDestroy, sceSpPthreadMutexattrInit, sceSpPthreadMutexattrSetprioceiling, sceSpPthreadMutexattrSetprotocol, sceSpPthreadMutexattrSettype, sceSpPthreadSetprio and sceSpSelect](../../../core/libs/prx/libSceSpSysCallWrapper/Export.cpp) (libSceSpSysCallWrapper) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSpSysCallWrapper.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/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 20e9e19b1d..cbd359521e 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -2,13 +2,15 @@ ### Unknown function info -- [CA_MGMT_extractKeyBlobEx, CA_MGMT_extractPublicKeyInfo, CA_MGMT_freeKeyBlob, CRYPTO_initAsymmetricKey, CRYPTO_uninitAsymmetricKey, RSA_verifySignature, VLONG_freeVlongQueue, sceSslCheckRecvPending, sceSslCreateSslConnection and sceSslDeleteSslConnection](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - the list is the intersection of the export table of the console's libSceSsl (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo. Names and NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); no credible prototypes were found. They are declared without parameters solely to resolve known imports, not as an ABI claim; callers must not call them until signatures are verified. +- [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. - [sceSslReuseConnection](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - its name and NID appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); its parameter shape follows [SharpProspero's pinned binding](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs#L2231-L2235). No console observation or reliable implementation source for session reuse was found. -- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. The behaviour below was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title); what differs for a title process is unknown. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006) from `sceSslCreateConnection`, and every connection call on an unknown or deleted connection id or on a connection whose context was terminated returns `SSL_ERROR_INVALID_ID`. `sceSslCreateConnection` returns `SSL_ERROR_INVALID_VALUE` (0x8095F007) for a null host and does not check the socket: `sceSslConnect` on a descriptor that is not a socket returns `SCE_NET_ERROR_EBADF` (0x80410109). `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: the console offers TLS 1.2 only (its ClientHello carries 12 suites and no supported_versions extension) and a TLS 1.3-only server gets `SSL_ERROR_FATAL_ALERT` (0x8095F010) from it, as from this build. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`; a connection keeps the pool alive after `sceSslTerm`, whose connections return `SSL_ERROR_INVALID_ID` from then on. The console's pool use for one connection with a two-certificate chain peaked at 80,544 bytes in a 256 KiB pool (5,568 after `sceSslInit`, 12,000 after loading one CA); a 16 KiB pool fails the connect with `SSL_ERROR_OUT_OF_MEMORY` (0x809517D5) there and here. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, the host name the connection was created with (case-insensitively; an IP literal sends no SNI and matches an IP subject alternative name; an empty host sends an empty SNI and fails `CN_CHECK`) and, when enabled, its validity dates, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits, codes and initial options); no root bundle is shipped and the console's built-in roots are unknown, so with no CA loaded a connection with the initial options returns `SSL_ERROR_UNKNOWN_CA` (0x8095F00C), as on the console. A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. A failed verification is final: close_notify is sent, later `sceSslConnect` calls return the same code (also after the `WANT_POLLIN` rounds of a non-blocking socket), and `sceSslRead` and `sceSslWrite` throw, as the console's codes for them were not measured. With a verify callback set it is called once after the handshake, outside the connection's lock, with the context id, the bits of the enabled checks that failed (`KNOWN_CA_CHECK` 0x20, `CN_CHECK` 0x4, the date checks when enabled, 0 when the chain passed), handles to a copy of the peer chain as received (the leaf first, the root last when the server sent it) that the certificate readers of the connection's context accept until the connection is deleted (the handles stay registered in the context until it is terminated; the console frees them at the delete), the chain length and the user argument. A chain fault that no option names (`SSL_ERROR_INVALID_CERT` without a callback) throws when a callback is set, as the bits the console passes for it were not measured. A non-negative return accepts the connection whatever the bits were; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and the callback runs again, with the same chain, from every later `sceSslConnect`, `sceSslRead` and `sceSslWrite` of the connection, which return 0x8095F0FD while it keeps refusing and proceed once it accepts. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. `sceSslRead` and `sceSslWrite` before `sceSslConnect` run the handshake and verification themselves and return its code when it fails. A non-blocking socket that would block returns `SSL_ERROR_WANT_POLLIN` (0x8095F013) or `SSL_ERROR_WANT_POLLOUT` (0x8095F014) from `sceSslConnect`, `sceSslRead` and `sceSslWrite`; the next `sceSslConnect` resumes the handshake, and `sceSslWrite` returns the part it sent before blocking (the console returned 32 KiB of a 64 KiB write). A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_WANT_POLLIN` as well, and the call can be repeated; `SSL_ERROR_READ_TIMEOUT` (0x80951710) is never returned. libkernel keeps its timeouts above the host socket, so they do not bound SSL reads and writes on a libkernel socket. `sceSslRead` with flags 0 reads until `len` bytes arrived or the stream ended and with flags 1 or 2 returns what the first record delivers. Native flags 3 peek without consuming data; values outside 0 through 3 return `SSL_ERROR_INVALID_VALUE` before connection lookup. A non-blocking read that got some bytes returns them. It returns 0 after the server's close_notify and after a transport that ends without one, every later read 0 again, and `SSL_ERROR_EOF` (0x8095177C) and `SSL_ERROR_SOCKET_CLOSED` (0x8095170D) never from a read. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) from `sceSslRead` and `SCE_NET_ERROR_EPIPE` (0x80410120) from `sceSslWrite`, on every later call too; a record that fails its MAC returns 0x80951D55 from `sceSslRead` after the fatal alert goes out. `sceSslWrite` accepts flags 0 only, `SSL_ERROR_INVALID_VALUE` otherwise, sends all of `len`, capped at `INT_MAX`, and returns the count; it keeps writing after the peer's close_notify or a stream that ended without one, as the console does. A fatal alert during the handshake returns `SSL_ERROR_FATAL_ALERT` and ends the transport: later `sceSslConnect` calls return 0, `sceSslRead` 0 and `sceSslWrite` `SSL_ERROR_SOCKET_CLOSED`, as measured. A transport that ends without close_notify or a reset during the handshake returns `SSL_ERROR_SOCKET_CLOSED`, the session is dropped and the next `sceSslConnect` starts over; those two handshake cases and `SSL_ERROR_INVALID_CERT` (0x8095F00A) for a verification failure no option names were not measured. Other handshake and socket failures, a read or write after a failed verification, a write after `sceSslClose`, a null buffer and a read or write from inside the callback throw, as their console codes are unknown. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify and returns `SSL_ERROR_INVALID_ID` when repeated. `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, which then returns `SSL_ERROR_INVALID_ID`; a second `sceSslTerm` returns `SSL_ERROR_INVALID_ID` and `sceSslInit` with a pool size of 0 returns `SCE_KERNEL_ERROR_EINVAL` (0x80020016). `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. The console's first `sceSslConnect` in a process took 23.8 s before its ClientHello left, a one-time initialisation not emulated. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts +- [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) there and here. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, the host name the connection was created with (case-insensitively; an IP literal sends no SNI and matches an IP subject alternative name; an empty host sends an empty SNI and fails `CN_CHECK`) and, when enabled, its validity dates, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits, codes and initial options); no root bundle is shipped and the console's built-in roots are unknown, so with no CA loaded a connection with the initial options returns `SSL_ERROR_UNKNOWN_CA` (0x8095F00C), as on the console. A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. A failed verification is final: close_notify is sent, later `sceSslConnect` calls return the same code (also after the `WANT_POLLIN` rounds of a non-blocking socket), and `sceSslRead` and `sceSslWrite` throw, as the console's codes for them were not measured. With a verify callback set it is called once after the handshake, outside the connection's lock, with the context id, the bits of the enabled checks that failed (`KNOWN_CA_CHECK` 0x20, `CN_CHECK` 0x4, the date checks when enabled, 0 when the chain passed), handles to a copy of the peer chain as received (the leaf first, the root last when the server sent it) that the certificate readers of the connection's context accept until the connection is deleted (the handles stay registered in the context until it is terminated; the console frees them at the delete), the chain length and the user argument. A chain fault that no option names (`SSL_ERROR_INVALID_CERT` without a callback) throws when a callback is set, as the bits the console passes for it were not measured. A non-negative return accepts the connection whatever the bits were; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and the callback runs again, with the same chain, from every later `sceSslConnect`, `sceSslRead` and `sceSslWrite` of the connection, which return 0x8095F0FD while it keeps refusing and proceed once it accepts. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. `sceSslRead` and `sceSslWrite` before `sceSslConnect` run the handshake and verification themselves and return its code when it fails. A non-blocking socket that would block returns `SSL_ERROR_WANT_POLLIN` (0x8095F013) or `SSL_ERROR_WANT_POLLOUT` (0x8095F014) from `sceSslConnect`, `sceSslRead` and `sceSslWrite`; the next `sceSslConnect` resumes the handshake, and `sceSslWrite` returns the part it sent before blocking (the console returned 32 KiB of a 64 KiB write). A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_WANT_POLLIN` as well, and the call can be repeated; `SSL_ERROR_READ_TIMEOUT` (0x80951710) is never returned. libkernel keeps its timeouts above the host socket, so they do not bound SSL reads and writes on a libkernel socket. `sceSslRead` with flags 0 reads until `len` bytes arrived or the stream ended and with flags 1 or 2 returns what the first record delivers. Native flags 3 peek without consuming data; values outside 0 through 3 return `SSL_ERROR_INVALID_VALUE` before connection lookup. A non-blocking read that got some bytes returns them. It returns 0 after the server's close_notify and after a transport that ends without one, every later read 0 again, and `SSL_ERROR_EOF` (0x8095177C) and `SSL_ERROR_SOCKET_CLOSED` (0x8095170D) never from a read. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) from `sceSslRead` and `SCE_NET_ERROR_EPIPE` (0x80410120) from `sceSslWrite`, on every later call too; a record that fails its MAC returns 0x80951D55 from `sceSslRead` after the fatal alert goes out. `sceSslWrite` accepts flags 0 only, `SSL_ERROR_INVALID_VALUE` otherwise, sends all of `len`, capped at `INT_MAX`, and returns the count; it keeps writing after the peer's close_notify or a stream that ended without one, as the console does. A fatal alert during the handshake returns `SSL_ERROR_FATAL_ALERT` and ends the transport: later `sceSslConnect` calls return 0, `sceSslRead` 0 and `sceSslWrite` `SSL_ERROR_SOCKET_CLOSED`, as measured. A transport that ends without close_notify or a reset during the handshake returns `SSL_ERROR_SOCKET_CLOSED`, the session is dropped and the next `sceSslConnect` starts over; those two handshake cases and `SSL_ERROR_INVALID_CERT` (0x8095F00A) for a verification failure no option names were not measured. Other handshake and socket failures, a read or write after a failed verification, a write after `sceSslClose`, a null buffer and a read or write from inside the callback throw, as their console codes are unknown. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify and returns `SSL_ERROR_INVALID_ID` when repeated. A second `sceSslTerm` returns `SSL_ERROR_INVALID_ID` and `sceSslInit` with a pool size of 0 returns `SCE_KERNEL_ERROR_EINVAL` (0x80020016). `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. The console's first `sceSslConnect` in a process took 23.8 s before its ClientHello left, a one-time initialisation not emulated. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - takes the connection id, as in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252). Measured on 13.40 as the connection entry describes: it sends close_notify once if the handshake completed and returns 0, 0 again when repeated and 0 on a connection that never connected, already failed verification or lost its transport to a fatal alert; a close before the handshake changes nothing, so the connection can still connect. After the close `sceSslConnect` returns 0 and `sceSslRead` 0, as on the console; `sceSslWrite` after it throws, as the console's code was not measured. A close_notify that a non-blocking socket cannot take at once returns `SSL_ERROR_WANT_POLLOUT` (0x8095F014) and the close can be repeated; after a reset it returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) every time. The socket is left to the title; an unknown connection returns `SSL_ERROR_INVALID_ID` (0x8095F006). Whether the console closes the socket is unknown - [Certificate readers](../../../core/libs/prx/libSceSsl/Export.cpp): foreign and expired certificate/name handles throw instead of following native unchecked pointers. Name views track certificate lifetime and preserve identity; the native no-op name release is implemented. View storage remains allocated until context destruction. Numeric names for attributes Mbed TLS cannot identify may differ from native names. Fingerprint stack-space validation and backend digest failures are not reproduced. Signatures, valid-context/null-argument precedence, serial truncation, name copy bounds, validity timestamps and supported hash selectors are confirmed by the supplied module. - [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS parses DER/PEM chains. CA imports still commit together instead of preserving successful entries before a later failure. Client certificate replacement clears the previous identity; raw private keys load independently and are parsed when configuring TLS. Partial client chains on decode failure, native key parse errors and incomplete identities during TLS setup remain unsupported. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. Native CA queries include built-in roots; the host still queries loaded authorities and returns `NOT_FOUND` for an empty context. No native root bundle is copied. Null-output and invalid-context precedence, output preservation on invalid contexts and the SDK 7.50 unload error boundary are recovered. Legacy clearing of foreign raw CA lists remains unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials does not start a TLS connection; `sceSslConnect` verifies the server against the loaded CAs and offers the loaded client certificate - [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs), the source of the other connection signatures; the behaviour 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). The verify options select which verification failures `sceSslConnect` reports (see the connection entry above), with the bit values SharpProspero's `SceSslVerifyOption` gives: `SERVER_VERIFY` 0x1, `CN_CHECK` 0x4, `NOT_AFTER_CHECK` 0x8, `NOT_BEFORE_CHECK` 0x10, `KNOWN_CA_CHECK` 0x20. The initial options are `SERVER_VERIFY`, `CN_CHECK` and `KNOWN_CA_CHECK` (0x25): the console accepted an expired and a not-yet-valid server certificate until 0x8 or 0x10 was enabled, which then returned `SSL_ERROR_NOT_AFTER_CHECK` (0x8095F00D) and `SSL_ERROR_NOT_BEFORE_CHECK` (0x8095F00E); whether 0x2 (`CLIENT_VERIFY`) starts enabled was not determined and it is kept off. A check that is disabled is not reported, whatever the chain. Bits outside 0x3F return `SSL_ERROR_INVALID_VALUE` (0x8095F007) from both calls. Disabling 0x1, 0x4 or 0x20 from the payload stopped its process through the kernel's debug exception path (`mDBG: Sending signal ... signo: 0xa0020101`, 6 of 6 attempts; disabling 0x2, 0x8 and 0x10 returned 0), so whether a title may clear them is unknown; this build clears them, as titles import the call. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values 2 (SSL 3.0) to 5 (TLS 1.2) and returns `SSL_ERROR_INVALID_VALUE` for 0, 1 and anything above 5; the console has no TLS 1.3 value. Every accepted value gives the same handshake, as Mbed TLS 3 speaks TLS 1.2 and nothing older and TLS 1.3 is compiled out, which matches the console offering TLS 1.2 only. `sceSslSetAlpn` stores the string up to its terminator, split on commas into the protocols offered in the handshake (the console sent `h2,http/1.1` as the two names and a wire-format string as one name), a second call replaces the list, an empty string returns `SSL_ERROR_BAD_LENGTH` (0x80951773), and a null list clears the configured protocols. Native inspection confirms that replacement clears the old list even on failure, that each comma-separated name must contain 1-255 bytes and that the whole string must be shorter than 65535 bytes. `sceSslGetAlpnSelected` returns the protocol the server selected, still after `sceSslClose`, or null before the handshake and when none was selected; the pointer is owned by the connection. An unknown connection and a connection whose context was terminated return `SSL_ERROR_INVALID_ID` (0x8095F006) from the four setters and null from `sceSslGetAlpnSelected`, as confirmed by the supplied native module - [sceSslGetPem](../../../core/libs/prx/libSceSsl/Export.cpp): native argument validation, CRLF line endings, 64-character Base64 lines, NUL-inclusive length and buffer clearing on insufficient capacity are recovered. Foreign and expired certificate handles throw instead of performing native unchecked pointer access. DER reserialization differences for noncanonical certificates remain unverified. + +- This pending declaration has an unverified signature and behavior; it is a parameterless import-resolution declaration and throws through `NotImplemented_nid_no_patch`: [`sceSslSetServerName`](../../../core/libs/prx/libSceSsl/Export.cpp). diff --git a/docs/dev/debt/libSceSulphaDrv.md b/docs/dev/debt/libSceSulphaDrv.md index 88b6f9091f..990eaa1951 100644 --- a/docs/dev/debt/libSceSulphaDrv.md +++ b/docs/dev/debt/libSceSulphaDrv.md @@ -1,5 +1,5 @@ # libSceSulphaDrv technical debt -### Inventory exports declared elsewhere +### Unknown function info -sceSulphaGetClientInfo, sceSulphaGetConnectedClientCount, sceSulphaGetDefaultConfig, sceSulphaGetNeededMemory, sceSulphaInit, sceSulphaMessage, sceSulphaSetBookmark and sceSulphaShutdown are the complete PS5 3.20 export table of this module per the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSulphaDrv.c), but all eight are currently declared by #7416 in libSceSulpha, so this module keeps its `APS5_DUMMY_FUN` skeleton to respect the duplicate-export rule. Declare them here (with no parameters, matching the `1b5004d77` unknown-prototype shape) if that PR's module attribution is rejected. +- [libSceSulphaDrv](../../../core/libs/prx/libSceSulphaDrv/Export.cpp) - signatures and behavior remain unknown for `sceSulphaGetClientInfo`, `sceSulphaGetConnectedClientCount`, `sceSulphaGetNeededMemory`, `sceSulphaInit`, `sceSulphaMessage`, `sceSulphaSetBookmark` and `sceSulphaShutdown`. Their no-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch`; they make no ABI claim. diff --git a/docs/dev/debt/libSceSysmodule.md b/docs/dev/debt/libSceSysmodule.md index 042c89ea84..7049c717ee 100644 --- a/docs/dev/debt/libSceSysmodule.md +++ b/docs/dev/debt/libSceSysmodule.md @@ -2,7 +2,7 @@ ### Silent stubs -- [sceSysmoduleIsLoaded, sceSysmoduleLoadModule, sceSysmoduleUnloadModule](../../../core/libs/prx/libSceSysmodule/Export.cpp) (libSceSysmodule) accept a module id missing from the module table: they log it and return 0 without loading anything, so `sceSysmoduleIsLoaded` reports an unknown module as loaded and unloading it succeeds, because titles that load or unload such an id aborted otherwise. The internal variants (`sceSysmoduleLoadModuleInternal`, `sceSysmoduleUnloadModuleInternal`, `sceSysmoduleLoadModuleInternalWithArg`, `sceSysmoduleUnloadModuleInternalWithArg`, `sceSysmoduleGetModuleHandleInternal`) throw for an unknown id. What the console returns for an id it does not know is not verified. +- [sceSysmoduleIsLoaded, sceSysmoduleLoadModule, sceSysmoduleUnloadModule](../../../core/libs/prx/libSceSysmodule/Export.cpp) (libSceSysmodule) accept the 141 ids `sceSysmoduleIsLoaded` did not reject in a payload process on a PS5 (firmware 13.60) and return `0x805A1000` for every other id, as the console does; that includes 51 ids the module table names from older firmware, such as 0x0001 (`libSceAudioOut`) and 0x0080 (`libSceJson`). Load and unload are counted as measured. The 21 accepted ids the table does not name (0x0111, 0x0117, 0x011c to 0x011f, 0x0121 to 0x0124, 0x0129, 0x012c to 0x012e, 0x0133, 0x0134, 0x0136 and 0x013a to 0x013d) are logged and return 0 without loading anything, so `sceSysmoduleIsLoaded` reports them as loaded; which module each loads is not known. Loading id 0x00ff ended the payload process. One title (PPSA04354) calls `sceSysmoduleIsLoaded` with id 0x96 at twelve sites and loads it only when the result is 0x805a1001; any other result lets it continue. ### Unknown function info diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 40dfa92cfa..3b33eca585 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -4,6 +4,8 @@ - [sceShellCoreUtilActivateAbort, sceShellCoreUtilActivateStart, sceShellCoreUtilActivateStartAsync, sceShellCoreUtilActivateTerm](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) return zero with the recovered handle argument. Activation service state is not modeled. +- [sceShellCoreUtilActivateRecordActivation](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) - activation service state is not modeled; this parameterless export returns zero without recording an activation. + - [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 - [sceSystemServiceLaunchWebBrowser](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 launches no web browser, so the call fails and nothing is shown. RommPS PPSA76677 then tells the player to scan its QR code instead - [sceSystemServiceShowControllerSettings](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without showing the controller settings: AnyPS5 has no system UI to open. Cat Quest III (PPSA15065) imports it @@ -13,6 +15,8 @@ ### Unknown function info +- [sceShellCoreUtilActivateGetStatus, sceShellCoreUtilActivateInit, sceShellCoreUtilActivateIsActivated](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) - the activation contract is unresolved: handle values and lifecycle, the activation flag, status/result fields, null-output errors and repeated initialization are unverified. All calls throw `NotImplemented_nid_no_patch`, including calls with null outputs. + - [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. - [sceSystemServiceGetAppId, sceSystemServiceGetAppTitleId, sceLncUtilGetAppTitleId](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the console's results for a title ID or app ID other than the running title are unknown, and so are its errors for null pointers, so those calls throw. `sceSystemServiceGetAppId` compares the title ID exactly with the one in `param.json` or `param.sfo`. The title ID buffer is 10 bytes, so a running title ID of 10 or more characters throws too. The signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L236-L260), except that `sceLncUtilGetAppTitleId` returns an `int`, as declared in the pending exports, and 0 on success like `sceSystemServiceGetAppTitleId`; SharpProspero names `ErrorNoAppInfo` (0x80A10009) only for `sceSystemServiceGetAppFocusedAppStatus`. - [sceSystemServiceLaunchApp](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 runs one title and cannot start another, so every call throws without reading its arguments. The (title ID, null-terminated argv, launch parameters) signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L141-L148), whose `AppLauncher` Prospero Explorer (PPSA99109) and Prospero Multi Tools (PPSA99110) import it through and which passes a 0x20-byte launch context or null; the console's argument checks and errors are unknown diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index d539ce9772..c9e0118fab 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -7,10 +7,13 @@ - [libSceUlt ulthreads](../../../core/libs/prx/libSceUlt/Export.cpp) - Runtime option defaults still omit the native header, stack size, worker affinity and priority fields; affinity depends on unresolved process configuration queries. Concurrent join ownership is not modeled: the native runtime distinguishes an ordinary running thread from one with another joiner. Host threads do not use the guest context or worker scheduler. - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - Queue-data pool destruction and shared slot limits still require comparison with the native node allocator. - [`sceUltConditionVariableCreate` and `sceUltConditionVariableOptParamInitialize`](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) are absent from the console export list, where only the underscored variants `_sceUltConditionVariableCreate` and `_sceUltConditionVariableOptParamInitialize` appear; their non-underscored signatures are assumed. Condition variable option parameter initialization only zeroes 128 bytes, and its layout without the common header remains unverified on hardware. +- [Status query exports](../../../core/libs/prx/libSceUlt/Export.cpp) lack waiter identities, types and wait values. All five synchronization queries throw before writing output when capacity is nonzero and waiters exist; capacity-zero queries still report counts. `sceUltGetUlthreadRuntimeInfo` throws for a non-empty worker array because no worker threads are started. Recovered layouts, waiting records, worker scheduling and priority accounting lack full console validation. +- [Mutex and reader-writer status queries](../../../core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp) throw for held recursive mutexes and ulthread-owned mutexes or write locks: recursive counts and ulthread owner encoding remain unverified. [Firmware 13.42 observations](https://github.com/boykopovar/AnyPS5/pull/9975#issuecomment-6417685562), from native x86-64 module runs with stubbed imports rather than PS5 hardware, supply `lockType` 1/2/0 for read/write/free, pthread `owner` from the thread block's first 8 bytes, `ownerType` 1 and default mutex `lockCount` 0 when held once. Both queries reject any supplied waiting record whose `size` is not 64 with `ULT_ERROR_INVALID` before writing output, as observed without waiters. Validation precedence and record-size checks for the other queries remain unverified. - [Ult factory and creation exports](../../../core/libs/prx/libSceUlt/Export.cpp) without a leading underscore are absent from the supplied module; their signatures remain unresolved. Diagnostic signatures and synchronization argument lists are recovered. - [sceUltMutexTryLock, the reader-writer lock and sceUltUlthreadGetSelf, sceUltUlthreadYield and sceUltUlthreadExit](../../../core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp) (libSceUlt) - the signatures follow the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.cpp) (a 256-byte lock and 128-byte option structures), not a PS5 binary. The rules are assumed: readers may take the lock while a writer waits, a thread that holds the write lock gets `ULT_ERROR_STATE` for any further lock instead of waiting on itself, a reader asking for the write lock waits on its own read lock, and the try forms return `ULT_ERROR_BUSY`. Outside an ulthread, `sceUltUlthreadGetSelf`, `sceUltUlthreadYield` and `sceUltUlthreadExit` return `ULT_ERROR_STATE`; inside one they act on the guest thread it runs on, since ulthreads are not scheduled cooperatively here ### Functional +- [sceUltFinalize](../../../core/libs/prx/libSceUlt/Export.cpp) - allocator-failure behavior is unverified on a PS5. The host may throw `std::bad_alloc` while reserving temporary wake lists; guest waiting-pool exhaustion and native finalization errors are not modeled. - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; the guest object layouts remain incomplete. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. - [sceUltWaitingQueueResourcePoolDestroy](../../../core/libs/prx/libSceUlt/Export.cpp) refuses with `ULT_ERROR_STATE` only when a thread is currently waiting on a pool-backed mutex, semaphore or queue, matching the PS5 libSceUlt disassembly (0x473a in or55417wcDk); bound but idle objects do not block pool destruction. Waiting-queue pool guest work area node accounting is not implemented. diff --git a/docs/dev/debt/libSceUsbd.md b/docs/dev/debt/libSceUsbd.md index 3f234ef7b4..e7e535f68b 100644 --- a/docs/dev/debt/libSceUsbd.md +++ b/docs/dev/debt/libSceUsbd.md @@ -6,8 +6,10 @@ ### Unknown function info -- [sceUsbdClearHalt, sceUsbdDetachKernelDriver, sceUsbdEventHandlerActive, sceUsbdGetDevice, sceUsbdGetDeviceSpeed, sceUsbdGetMaxIsoPacketSize, sceUsbdGetMaxPacketSize, sceUsbdHandleEvents, sceUsbdHandleEventsLocked, sceUsbdLockEventWaiters, sceUsbdLockEvents, sceUsbdOpenDeviceWithVidPid, sceUsbdSetInterfaceAltSetting, sceUsbdTryLockEvents, sceUsbdUnlockEventWaiters, sceUsbdUnlockEvents, sceUsbdWaitForEvent](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - signatures follow the console type database in [orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L2268-L2322), but their behavior is unknown and each call throws; not measured on a console +- [sceUsbdEventHandlerActive, sceUsbdHandleEvents, sceUsbdHandleEventsLocked, sceUsbdLockEventWaiters, sceUsbdLockEvents, sceUsbdTryLockEvents, sceUsbdUnlockEventWaiters, sceUsbdUnlockEvents, sceUsbdWaitForEvent](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - signatures follow the console type database in [orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L2268-L2322), but their behavior is unknown and each call throws; not measured on a console ### Functional - [sceUsbdGetConfiguration, sceUsbdGetConfigDescriptorByValue, sceUsbdGetDescriptor, sceUsbdGetStringDescriptorAscii](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - not measured on a console; they follow FreeBSD 11.4 ([libusb10_desc.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10_desc.c), [libusb20.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb20.c)). The configuration and descriptor queries return host libusb errors. `sceUsbdGetStringDescriptorAscii` rebuilds `libusb20_dev_req_string_simple_sync` with only its 255-byte read: libusb's Windows HID backend reports the requested length as `bLength`, cutting a 4-byte header read to one character. Unlike the host `libusb_get_string_descriptor_ascii` it accepts index 0, drops non-printable characters and `<>` instead of writing `?`, and fails with `0x802400FF`. Closes when: measured on a PS5 +- [sceUsbdGetDevice, sceUsbdGetDeviceSpeed, sceUsbdGetMaxPacketSize, sceUsbdGetMaxIsoPacketSize, sceUsbdOpenDeviceWithVidPid](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - not measured on a console; they follow FreeBSD 11.4 [libusb10.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10.c) over the host libusb. The packet sizes are searched in the active configuration and the iso size is scaled as FreeBSD does, not through the host's versions, which differ for an unreadable configuration and for SuperSpeed endpoints. A host SuperSpeed+ device reports SuperSpeed, the fastest speed FreeBSD 11.4 has. A null device in `sceUsbdGetMaxIsoPacketSize` throws, as FreeBSD dereferences it. A device stays known while a handle to it is open, so the device of a handle from `sceUsbdOpenDeviceWithVidPid` can be used until that handle is closed. Closes when: measured on a PS5 +- [sceUsbdSetInterfaceAltSetting, sceUsbdClearHalt, sceUsbdDetachKernelDriver](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - not measured on a console; they follow FreeBSD 11.4 [libusb10.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10.c) over the host libusb. `sceUsbdSetInterfaceAltSetting` checks the interface range and this library's record of claimed interfaces, and returns `0x802400FF` for any other failure; it throws while the device has transfers in flight, which FreeBSD cancels first. `sceUsbdClearHalt` returns 0, or `0x802400FF` for an endpoint the host does not find; FreeBSD ignores the result of the clear itself, so the host's other errors throw, and so does a null handle, which FreeBSD dereferences. `sceUsbdDetachKernelDriver` returns `0x802400FF` for any failure, which on Windows is every call, as libusb cannot detach drivers there. Closes when: measured on a PS5 diff --git a/docs/dev/debt/libSceUserService.md b/docs/dev/debt/libSceUserService.md index f1cb7b914e..010782b062 100644 --- a/docs/dev/debt/libSceUserService.md +++ b/docs/dev/debt/libSceUserService.md @@ -8,5 +8,5 @@ - [sceUserServiceGetUserName](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - `0x80960105`, which a PS5 (firmware 13.60) returns for a user id it does not know, has no known name; it is `USER_SERVICE_ERROR_UNKNOWN_USER` here. On the console a registered user who is not logged in still gets their name; here up to 4 users ("Player", "Player2", "Player3", "Player4") are supported; user names "Player2", "Player3" and "Player4" are assumed, not measured - [sceUserServiceGetEvent](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - login and logout events queued dynamically on controller hotplug have no source and are assumed, not measured - [sceUserServiceGetAgeLevel](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - on a PS5 (firmware 13.60) the value is the age set in the user's parental restriction (0, 7, 12 and 15 were measured) and `0xFFFFFFFF` without one; a user who is not logged in returns `0x80960009` with or without an output. Parental restrictions are not modelled, so the initial user is always reported as unrestricted. PPSA12544 (Unity) only stores the value for its user profile -- [sceUserServiceGetAccessibilityChatTranscription, PressAndHoldDelay, TriggerEffect, Vibration, ZoomEnabled, ZoomFollowFocus](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - report 0, 0, 1, 1, 0 and 1, what a PS5 (firmware 13.60) reports for a user whose accessibility settings are all at their defaults; the console reports the user's own settings (a changed vibration intensity read 3), which are not modelled. What each number means is unknown. A user who is not logged in returns `0x80960009` with or without an output +- [sceUserServiceGetAccessibilityChatTranscription, PressAndHoldDelay, TriggerEffect, Vibration, ZoomEnabled, ZoomFollowFocus](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - report 0, 0, 1, 1, 0 and 1, what a PS5 (firmware 13.60) reports for a user whose accessibility settings are all at their defaults; the console reports the user's own settings (a changed vibration intensity read 3), which are not modelled. What each number means is unknown. A user who is not logged in returns `0x80960009` with or without an output. One title (PPSA04354) disassembled for this entry calls all six in a row (first at 0xea0efa) with its user ID, only when a flag bit (0x10) is set; a result other than 0 from any of them ends the block and is returned, and only when all six return 0 does it store the six values in its object. - [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. diff --git a/docs/dev/debt/libSceVdecsw.md b/docs/dev/debt/libSceVdecsw.md index 4818e0bfea..ca1399ba19 100644 --- a/docs/dev/debt/libSceVdecsw.md +++ b/docs/dev/debt/libSceVdecsw.md @@ -5,3 +5,5 @@ - [libSceVdecsw](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - structure layouts follow shadPS4's `vdecsw.h` and were checked against PPSA05684's movie decoder (`movie-decoder-vdecsw.cpp`), which asserts `pictureCount == 1` at offset 0xb of `OutputInfo` and reads `InputResult` offset 0x10 as the number of frames an access unit produced. The compute, decoder memory and decode input structures and the first 0x28 bytes of the 0x50-byte decoder config are those of libSceVideodec2, and decoding goes through it. The 0x78-byte AVC picture info is filled with Videodec2's leading fields and the cropping fields only. OUTPUT_PENDING (0x81510115), INPUT_QUEUE_EMPTY (0x81510116) and DECODE_PENDING (0x81510117) follow from the codes each call's assert accepts. At most four decoded pictures are queued and the last one is held until `sceVdecswFinalizeDecodeSequence`; the console's queue depth is unknown. `sceVdecswSyncDecodeInput` / `sceVdecswSyncDecodeOutput` retry the TrySync variants until they stop returning DECODE_PENDING / OUTPUT_PENDING; they do not sleep beyond a yield, so another thread must drain outputs when the picture queue is full. The 0xb8-byte HEVC picture info (shadPS4's `vdecsw.h`) carries only the timestamps, coded size, bit depth, IDR and IRAP flags, profile, level and conformance window (in chroma samples, repeated in the frame cropping fields, whose console unit is unknown) - [sceVdecswGetVp9PictureInfo](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - no console behaviour is recorded; the signature and 0x58-byte layout come from [SharpProspero's Vdecsw.cs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Vdecsw.cs#L892-L952), and shadPS4 has no VP9 getter. It copies the leading `thisSize - 8` bytes of the picture's info, as [shadPS4's `sceVdecswGetPictureInfo`](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/videodec/vdecsw.cpp#L303-L334) does for AVC and HEVC, from Videodec2's VP9 info (`level` is 0 there). An output with no picture returns 0 and writes nothing. Null pointers, `thisSize` below 8, two pictures and a frame buffer no VP9 decoder returned (an H.264 picture too) throw; console error codes are unknown. VP9 decoding goes through Videodec2 and rejects pictures deeper than 10 bits + +- [sceVdecswGetPictureInfo](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - console behaviour and errors are unmeasured. The signature, 0x30/0x38 output descriptor sizes, optional first output, unchanged size field and empty-picture handling follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/videodec/vdecsw.cpp#L303-L334). Metadata comes from the existing AVC, HEVC or VP9 delivered-frame records, not the guest frame-buffer tail; copying is capped at the known record size, as in the typed getters. Invalid descriptor pointers/sizes, result sizes below 8 and unknown frame buffers throw instead of returning unverified console error codes. Two-picture field output remains unimplemented and throws before writing; the second output is untouched for zero or one picture. Metadata completeness and decoder limitations above still apply. diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index 48a3bade68..33aa777e33 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -9,6 +9,7 @@ ### Unknown function info +- PPSA29866 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceVideoOutSetFlipMaster and sceVideoOutClearFlipMaster](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut) - [sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_, sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_, sceVideoOutAdjustColorSpaceConversion_, sceVideoOutSetGlobalBlendSpace](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - PPSA30630 imports these to configure color space conversion and global blend space. Color space conversion settings are accepted with size validation (32 bytes) but conversion blocks and blend space parameters are not applied to the presentation swapchain - [sceVideoOutDeleteVrrActiveStatusEvent](../../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut, NID `-q-vbO859Tw`) - the name hashes to the NID PPSA02530 imports and is listed in [sce_symbols](https://github.com/zecoxao/sce_symbols) `aerolib.csv` at `fe2cc2948e`. The signature `(eq, handle)` and the behaviour, removing the handle's VRR status registration from the queue as the other `Delete*Event` calls do, follow sceVideoOutAddVrrActiveStatusEvent; no title has been observed calling it - [sceVideoOutCursorEnable, sceVideoOutCursorDisable, sceVideoOutCursorSetImageAddress, sceVideoOutCursorSetPosition](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NIDs `UPZWCH8qTM4`, `fEPEwROdCwM`, `u-9bhWQApq8`, `N17AK88NdD0`) - the names hash to the NIDs Prospero Win (PPSA99995) imports, and the parameter lists `(handle, index, address)`, `(handle, index)`, `(handle, index, address)` and `(handle, index, x, y)` follow its [declarations](https://github.com/mpereiraesaa/prospero-win/blob/644e32e400c555e340d507383398c5d21b39b0b0/wine/ps5/pw_vulkan_radv.c#L65-L68). The cursor image's format and size and the console's return values are not known; the functions report that they are not implemented. @@ -31,7 +32,7 @@ - [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - opening a bus that is already open returns `SCE_VIDEO_OUT_ERROR_RESOURCE_BUSY` (`0x80290009`), as [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/videoout/driver.cpp#L53-L60) PS4 driver does for its main port; the PS5 result is unverified. PPSA99764 opens the main bus twice and carries on, presenting through the first handle - [sceVideoOutOpen, bus 0x20](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - PPSA05666 opens the PS VR2 output as `sceVideoOutOpen(255, 0x20, 0, NULL)` after `sceHmd2ReprojectionEnableVrMode(0x2000F)`, and its own log calls the bus `SCE_VIDEO_OUT_BUS_TYPE_HMD2_MAIN`. The console answers `0x80290001` from a process that is not a game, with the headset connected and worn or not, for users 255 and 0 and for a null or zeroed open parameter, so that is returned here before the other argument checks; what a game gets with a headset attached is not measured. On the handle the title then calls `sceVideoOutRegisterBuffers2` with set 0, two buffers, a 4000x2040 attribute (tiling 0, pitch 0, pixel format `0x8100000022000000`), category 16 and a null option, where each `VideoOutBuffers.data` points at a 16-byte `{pointer, 2}` whose pointer is the structure it passed to `sceHmd2ReprojectionEnableMirroring`, holding per-eye 2000x2040 texture descriptors rather than frame memory; then `sceVideoOutConfigureOutput` and `sceVideoOutIsOutputSupported` with mode `0x2000F`, and the GPU waits on buffer 0 of that handle. None of that is modelled: the HMD output needs a VR backend. With the error code as its handle the title's next call, `sceVideoOutRegisterBuffers2`, throws here for the unknown handle; the console's answer to it is not measured. The TV bus answers `0x8029001E` to the HMD modes `0x20001` and `0x2000C` to `0x20010` on the console, as here -- [Overlay output bus](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - flips on a port opened with `SCE_VIDEO_OUT_BUS_TYPE_OVERLAY` complete and signal their events like main-bus flips, but their buffers are not shown: the overlay is not composited over the main output. Presenting them as whole frames made the window alternate between the two buses +- [Overlay output bus](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut, [libSceAgcDriver](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp)) - the overlay port's displayed buffer is blended over each main-bus presentation with premultiplied alpha on the encoded 8-bit values, each plane fit to the window. Unverified: premultiplied alpha is inferred from PPSA01670, whose 3840x2160 overlay held 527,020 translucent texels, none with a color channel above its alpha; linear-light blending and placing an overlay sized unlike the main buffer are unknown. 10-bit, PQ and float overlays throw. A new overlay shows at the next main flip. Over HDR10 it is not composited; CPU-decoded dumps (`APS5_NO_GPU_DUMP`, HDR10) hold the main plane only. A non-resident overlay is detiled on the CPU each present through a device-local image and a staging buffer of width x height x 4 bytes each per present slot (`FlipInFlight() + 1`, 2 by default) - [sceVideoOutSubmitEopFlip](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) reserves a flip and returns an opaque DWORD token; presentation is driven by a matching graphics-queue `RELEASE_MEM` with `INT_SEL` 2 and that token in `INT_CTXID`. This five-argument ABI and packet use are inferred from PPSA21564's bundled AGC driver, whose original packet-emission routine is checked in an isolated host ABI probe. The console token encoding, output width beyond the observed DWORD, error policy and reservation capacity are unverified. Host tokens occupy `0x04000000..0x07ffffff`, never repeat within a process, and exhaustion throws. Closing cancels unsubmitted reservations; only live reservations match; consumed or cancelled context IDs execute as ordinary label writes without targeting a reopened port. Async-queue EOP flips throw. Buffer registration, flip modes, queue capacity and vsync presentation follow the existing host VideoOut model, not measured console behavior. - [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 or while the pad's mouse-to-stick toggle is on). 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) - `sceVideoOutGetOutputStatus` reports dynamic range 2 and flag bit 0 (named HDR in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs)) only on Windows, while the window's Vulkan surface offers a 10-bit HDR10 swapchain with `VK_EXT_swapchain_colorspace` and `VK_EXT_hdr_metadata` and DXGI (`IDXGIOutput6::GetDesc1`) reports the window's monitor in `DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020`; `sceSystemServiceGetHdrToneMapLuminance` then reports the monitor's peak, full-frame peak and black level. A monitor that reports no usable peak gets a default of 1000 nits peak and full frame, 0 nits black. On Linux and other hosts the output stays SDR: a surface can offer HDR10 formats while the display is not in HDR (KDE Wayland with RADV lists them with the output's HDR disabled), and SDL2 and Vulkan expose no output HDR state; a compositor or output-state signal is needed first. The surface formats are read again when DXGI's monitor report changes and when the swapchain is recreated; a compositor that changes them without making the swapchain out of date is not seen until then. Every open port's output mode event fires when HDR output becomes available or unavailable and when its luminance changes; the console's events for an HDR change are unknown. Detection starts when VideoOut is first used: `sceSystemServiceGetHdrToneMapLuminance` called before that reports SDR, and no event fires for the availability found at start-up beyond the one every output-mode subscription gets when it is made. Console HDR settings (calibrated peak, the SDR-in-HDR conversion block) are not modelled diff --git a/docs/dev/debt/libSceVideoRecordingP.md b/docs/dev/debt/libSceVideoRecordingP.md index 0b82a50648..835fa5eb21 100644 --- a/docs/dev/debt/libSceVideoRecordingP.md +++ b/docs/dev/debt/libSceVideoRecordingP.md @@ -8,7 +8,6 @@ - [sceVideoRecordingCloseByHandle, sceVideoRecordingGetInfoByHandle](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - native handles are context pointers. Recording state and per-handle queries remain unimplemented. - [sceVideoRecordingCopyBGRAtoNV12](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - the destination, source, 32-bit width and height arguments are confirmed. Pixel conversion remains unimplemented. - [sceVideoRecordingGetSupportInfo, sceVideoRecordingGetVideoOutMode](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - GetSupportInfo writes an 18-byte payload whose fields remain unresolved. GetVideoOutMode takes a four-byte output confirmed by the native recording assembly and Avcap2 IPC descriptor; the mode values remain unresolved. Both exports remain unimplemented. -- [sceVideoRecordingInit, sceVideoRecordingTerm](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - parameterless initialization and termination stubs; unimplemented and throw `NotImplemented_nid_no_patch` - [sceVideoRecordingInitialize](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp): the native initializer consumes no caller arguments; the former optional parameter is not read by the Avcap2 initializer or recording setup. Its unused parameter slot is retained because the protected body references it. - [sceVideoRecordingOpen, sceVideoRecordingOpen2](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - the 32-byte and 40-byte parameter variants, four argument registers and 32-bit heap size are recovered. Mode, output mode, option and format semantics remain incomplete. QueryHddSize2 returns a signed 64-bit size or error; recording and sizing exports remain unimplemented. @@ -16,4 +15,4 @@ ### Functional -- [sceVideoRecordingGetStatus](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) always returns 0, `None` ("The recorder is not open") in [SharpProspero's `VideoRecordingStatus`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/VideoRecording.cs#L18-L29), also listed in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/video_recording/video_recording.h#L32-L36). It is true here because `sceVideoRecordingOpen` throws, so no recorder is ever open. Unverified: whether the console returns 0 or an error before `sceVideoRecordingInit`/`sceVideoRecordingInitialize`, which throw, and when it reports `Running` (1) or `Paused` (2) +- [sceVideoRecordingGetStatus](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) always returns 0, `None` ("The recorder is not open") in [SharpProspero's `VideoRecordingStatus`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/VideoRecording.cs#L18-L29), also listed in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/video_recording/video_recording.h#L32-L36). It is true here because `sceVideoRecordingOpen` throws, so no recorder is ever open. Unverified: whether the console returns 0 or an error before `sceVideoRecordingInit`/`sceVideoRecordingInitialize`, and when it reports `Running` (1) or `Paused` (2) diff --git a/docs/dev/debt/libSceVideodec2.md b/docs/dev/debt/libSceVideodec2.md index 74a8f81c0f..3b7fb61675 100644 --- a/docs/dev/debt/libSceVideodec2.md +++ b/docs/dev/debt/libSceVideodec2.md @@ -11,7 +11,7 @@ - [AVC parameter-set units](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) - an H.264 access unit with no slice NAL (types 1 to 5), such as the sequence and picture parameter sets Twispero (PPSA99474) submits on their own before a stream's first slices, is kept and put in front of the next unit that has a slice, and `sceVideodec2Decode` returns 0 with no picture for it, since libavcodec refuses a packet without a slice. What the console returns for such a unit is not known. HEVC units of parameter sets alone are still passed to libavcodec as they are -- [sceVideodec2CreateDecoder](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) decodes HEVC (codec type 974921) and VP9 (2382845) through FFmpeg, besides H.264. The tags come from SharpProspero's `Videodec2CodecType` and EVO-PLAYER-PS5's `sce_videodec2.h`, which records them from hardware runs. HEVC input is converted from length-prefixed NAL units to Annex B as for H.264, and VP9 frames are passed through. Output is NV12 for 8-bit pictures. A 10-bit picture is written as 16-bit little-endian words with the sample in the low 10 bits, Y rows then interleaved UV rows at `framePitchInBytes` times the picture height, with `framePitch` in samples and `framePitchInBytes` in bytes (twice the width, aligned to 256), and `QueryDecoderMemoryInfo` sizes a frame of an HEVC or VP9 profile 2 configuration with two bytes per sample. Evidence is from [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/tree/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783), which decodes and presents 1080p HEVC Main10 on hardware through this path (`docs/evo-pro/on-demand-decoder-handoff.md`): it takes a picture as 10-bit when `pitch_bytes >= pitch * 2`, puts UV at `pitch_bytes` times the height and samples with a low-bit factor (`projects/evoplayer/media/src/evo_vdec_native.c`, `ro_harvest`), and its hardware logs give 6128 KB and 24496 KB frame buffers for 1920x1088 and 3840x2176 Main10 decoders after rounding to 16 KiB. The 1080p size is what this formula gives; at 3840x2176 it gives 16 KiB less, so the console's size there has another term that is not known. Unverified: `frameFormat` (still 0; no consumer found reads it), VP9 profile 2 output (no hardware evidence), and the layout of 12-bit pictures and of decoded formats other than yuv420p10le, which throw. `sceVideodec2GetPictureInfo` and `sceVideodec2GetAvcPictureInfo` throw for HEVC pictures (`SceVideodec2HevcPictureInfo` is not filled), and `sceVideodec2GetHevcPictureInfo` reports that it is not implemented +- [sceVideodec2CreateDecoder](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) decodes HEVC (codec type 974921) and VP9 (2382845) through FFmpeg, besides H.264. The tags come from SharpProspero's `Videodec2CodecType` and EVO-PLAYER-PS5's `sce_videodec2.h`, which records them from hardware runs. HEVC input is converted from length-prefixed NAL units to Annex B as for H.264, and VP9 frames are passed through. Output is NV12 for 8-bit pictures. A 10-bit picture is written as 16-bit little-endian words with the sample in the low 10 bits, Y rows then interleaved UV rows at `framePitchInBytes` times the picture height, with `framePitch` in samples and `framePitchInBytes` in bytes (twice the width, aligned to 256), and `QueryDecoderMemoryInfo` sizes a frame of an HEVC or VP9 profile 2 configuration with two bytes per sample. Evidence is from [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/tree/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783), which decodes and presents 1080p HEVC Main10 on hardware through this path (`docs/evo-pro/on-demand-decoder-handoff.md`): it takes a picture as 10-bit when `pitch_bytes >= pitch * 2`, puts UV at `pitch_bytes` times the height and samples with a low-bit factor (`projects/evoplayer/media/src/evo_vdec_native.c`, `ro_harvest`), and its hardware logs give 6128 KB and 24496 KB frame buffers for 1920x1088 and 3840x2176 Main10 decoders after rounding to 16 KiB. The 1080p size is what this formula gives; at 3840x2176 it gives 16 KiB less, so the console's size there has another term that is not known. Unverified: `frameFormat` (still 0; no consumer found reads it), VP9 profile 2 output (no hardware evidence), and the layout of 12-bit pictures and of decoded formats other than yuv420p10le, which throw. `sceVideodec2GetPictureInfo` and `sceVideodec2GetAvcPictureInfo` throw for HEVC pictures (`SceVideodec2HevcPictureInfo` is not filled), and `sceVideodec2GetHevcPictureInfo` reports that it is not implemented. One title (PPSA02225) disassembled for this entry calls it once (arguments set up at 0x8b5095-0x8b50a6) with three pointers: a structure built on its stack, a structure inside its decoder object and the address of a field of the same object. A nonzero result makes it leave the decoder setup; on one path (a flag byte set and two 32-bit fields equal to values it computed before) it passes that field to the next Videodec2 call. The codec type it requests and the meaning of the structure fields are not shown. - [sceVideodec2GetVp9PictureInfo](../../../core/libs/prx/libSceVideodec2/Export.cpp) uses the 88-byte layout and two-pointer signature from [SharpProspero Videodec2.cs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs). Frame metadata follows the [VP9 specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.7-20170222-draft.pdf), sections 5.26 and 6.2: profile, key/intra-only flags, bit depth, color space/range, coded/render dimensions and superframe count are retained per emitted buffer. Level is 0 because the elementary stream does not signal a level; the console value is unverified. Null pointers, undersized structures and querying another codec throw; console error codes and support for other structure sizes are unknown. Pictures deeper than 10 bits are rejected. Superframes are submitted as individual coded frames to keep each output associated with its input metadata. `sceVideodec2GetPictureInfo` writes this layout for a VP9 picture, as shadPS4's `videodec2.cpp` writes the decoder's own layout; Mortal Shell 2 (PPSA34008) calls it with `thisSize` 88. - [sceVideodec2Decode after sceVideodec2Flush](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) without `sceVideodec2Reset` restarts FFmpeg's decoder (`avcodec_flush_buffers`) and decodes the unit, where it threw before. TEKKEN DARK RESURRECTION (PPSA31371), which runs in Sony's PSP emulator, decodes after a flush without a reset (#7118), so the console accepts the call. Pictures the flush decoded but has not returned yet are returned before the new ones. A flush with no picture pending (each decoded unit has returned its picture) leaves the decoder and its reference pictures as they are, so a stream that continues without a new IDR picture keeps decoding: Daxter (PPSA09695), also in the PSP emulator, flushes after every access unit of its H.264 movies, and restarting the decoder there lost every P picture until the next keyframe. What the console returns for pending pictures, and for a stream that continues across a flush that had pictures pending, is not measured. diff --git a/docs/dev/debt/libSceVisionManager.md b/docs/dev/debt/libSceVisionManager.md index 6e8c187895..f8f41d421e 100644 --- a/docs/dev/debt/libSceVisionManager.md +++ b/docs/dev/debt/libSceVisionManager.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceVisionManagerApplyCalibrateHmdResult, sceVisionManagerCancelRequest, sceVisionManagerClose, sceVisionManagerFinalize, sceVisionManagerGenerateRegisterUserData, sceVisionManagerGenerateRegisterUserDataFinalize, sceVisionManagerGenerateRegisterUserDataInitialize, sceVisionManagerGetCalibrateTrackingLedInfo, sceVisionManagerGetCalibrateTrackingLedNecessity, sceVisionManagerGetHitCountOfRegisterUserData, sceVisionManagerGetLibraryVersion, sceVisionManagerGetNumOfRegisterUserDataSlot, sceVisionManagerGetRegisterUserDataSize, sceVisionManagerGetRegisterUserDataVersion, sceVisionManagerGetResultOfCalibrateHmd, sceVisionManagerGetResultOfCalibrateStereoCamera, sceVisionManagerGetResultOfCalibrateTrackingLed, sceVisionManagerGetResultOfFindFace, sceVisionManagerGetResultOfFindPad, sceVisionManagerGetResultOfFindPadCorrespondRegion, sceVisionManagerGetResultOfFindUser, sceVisionManagerGetResultOfFindUserInRegion, sceVisionManagerGetResultOfFindUserWithoutIdentify, sceVisionManagerGetResultOfRegisterUserInRegion, sceVisionManagerGetStoreCameraImgYPtr, sceVisionManagerGetWorkingMemorySize, sceVisionManagerInitialize, sceVisionManagerLoadSettings, sceVisionManagerOpen, sceVisionManagerRegistryClear, sceVisionManagerRequestCalibrateHmd, sceVisionManagerRequestCalibrateStereoCamera, sceVisionManagerRequestCalibrateTrackingLed, sceVisionManagerRequestClose, sceVisionManagerRequestFindFace, sceVisionManagerRequestFindPad, sceVisionManagerRequestFindPadCorrespondRegion, sceVisionManagerRequestFindUser, sceVisionManagerRequestFindUserInRegion, sceVisionManagerRequestFindUserWithoutIdentify, sceVisionManagerRequestHeadDetection, sceVisionManagerRequestInitializeRegisterUserData, sceVisionManagerRequestOpen, sceVisionManagerRequestPadTracking, sceVisionManagerRequestRecogUtility, sceVisionManagerRequestRegisterUserInRegion, sceVisionManagerRequestStoreCameraImgY, sceVisionManagerSetCallbackForUpdateCalibrateHmdResult, sceVisionManagerSetCallbackForUpdateCalibrateStereoCameraResult, sceVisionManagerSetCallbackForUpdateCalibrateTrackingLedResult, sceVisionManagerSetCallbackForUpdateCameraFrame, sceVisionManagerSetCallbackForUpdateCtrlCore, sceVisionManagerSetCallbackForUpdateRecog, sceVisionManagerSetCallbackForUpdateRunningStatus, sceVisionManagerSetCallbackForUpdateStoreFrame and sceVisionManagerWaitResponse](../../../core/libs/prx/libSceVisionManager/Export.cpp) (libSceVisionManager) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVisionManager.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 +- [sceVisionManagerApplyCalibrateHmdResult, sceVisionManagerCancelRequest, sceVisionManagerFinalize, sceVisionManagerGenerateRegisterUserData, sceVisionManagerGenerateRegisterUserDataFinalize, sceVisionManagerGenerateRegisterUserDataInitialize, sceVisionManagerGetCalibrateTrackingLedInfo, sceVisionManagerGetCalibrateTrackingLedNecessity, sceVisionManagerGetHitCountOfRegisterUserData, sceVisionManagerGetLibraryVersion, sceVisionManagerGetNumOfRegisterUserDataSlot, sceVisionManagerGetRegisterUserDataSize, sceVisionManagerGetRegisterUserDataVersion, sceVisionManagerGetResultOfCalibrateHmd, sceVisionManagerGetResultOfCalibrateStereoCamera, sceVisionManagerGetResultOfCalibrateTrackingLed, sceVisionManagerGetResultOfFindFace, sceVisionManagerGetResultOfFindPad, sceVisionManagerGetResultOfFindPadCorrespondRegion, sceVisionManagerGetStoreCameraImgYPtr, sceVisionManagerGetWorkingMemorySize, sceVisionManagerInitialize, sceVisionManagerRegistryClear, sceVisionManagerRequestCalibrateHmd, sceVisionManagerRequestCalibrateStereoCamera, sceVisionManagerRequestCalibrateTrackingLed, sceVisionManagerRequestClose, sceVisionManagerRequestFindFace, sceVisionManagerRequestFindPad, sceVisionManagerRequestFindPadCorrespondRegion, sceVisionManagerRequestHeadDetection, sceVisionManagerRequestInitializeRegisterUserData, sceVisionManagerRequestOpen, sceVisionManagerRequestPadTracking, sceVisionManagerRequestRecogUtility, sceVisionManagerRequestStoreCameraImgY, sceVisionManagerSetCallbackForUpdateCalibrateHmdResult, sceVisionManagerSetCallbackForUpdateCalibrateStereoCameraResult, sceVisionManagerSetCallbackForUpdateCalibrateTrackingLedResult, sceVisionManagerSetCallbackForUpdateCameraFrame, sceVisionManagerSetCallbackForUpdateCtrlCore, sceVisionManagerSetCallbackForUpdateRecog, sceVisionManagerSetCallbackForUpdateRunningStatus, sceVisionManagerSetCallbackForUpdateStoreFrame and sceVisionManagerWaitResponse](../../../core/libs/prx/libSceVisionManager/Export.cpp) (libSceVisionManager) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVisionManager.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/libSceVoice.md b/docs/dev/debt/libSceVoice.md index 3dcdeae2a9..32d3ca5475 100644 --- a/docs/dev/debt/libSceVoice.md +++ b/docs/dev/debt/libSceVoice.md @@ -7,13 +7,13 @@ ### Unknown function info +- [sceVoiceVADAdjustment](../../../core/libs/prx/libSceVoice/Export.cpp) - throws through `NotImplemented_nid_no_patch`; behaviour and the PS5 prototypes are unmeasured. The only source of the parameter lists is shadPS4's PS4-derived prototypes (GPLv2, used as a reference only), with no second reference; structure pointers are `void*` because no layout is known. - [sceVoiceInit, sceVoiceStart, sceVoiceCreatePort, sceVoiceGetPortInfo](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - a second `sceVoiceInit` returns 0x804E0802, and Start, CreatePort and GetPortInfo return 0x804E0801 before checking any argument while the library is not initialized, as [prosper](https://github.com/mattias800/prosper/blob/ed42e499888d5a86792c3e1b2df3aebd8ca1a681/prosper/src/hle/audio/hle_audio.cpp#L4697-L4713) reads them from the shipped module. The other port and lifecycle calls still throw when the library is not initialized, since prosper gives no module address for them. Whether `sceVoiceInit` checks a null param before its state is unknown, so a null param still throws first, and that `sceVoiceEnd` returns the library to the not-initialized state is assumed. Closes when: the other entry points are read from the module or measured on a PS5 - [sceVoiceInitHQ, sceVoiceVADSetVersion](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - not measured on a PS5: the signatures and rules follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/voice/voice.cpp). `sceVoiceInitHQ` runs `sceVoiceInit`, as shadPS4's two share one path. `sceVoiceVADSetVersion` accepts versions 0 and 1 and returns `0x804E0805` for any other; nothing uses the version, as no voice activity detection runs - [sceVoicePausePortAll, sceVoiceResumePortAll](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - not measured on a PS5: both return 0x804E0801 while the library is not initialized and 0 otherwise, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c1b26558352b863f80142256e4df38585aa7d727/src/core/libraries/voice/voice.cpp#L356-L437); prosper reads the same code for `sceVoiceStart`, and the PS3 calls in [RPCS3](https://github.com/RPCS3/rpcs3/blob/be69c054eeac57e2e671697a463473d37fe5a7b2/rpcs3/Emu/Cell/Modules/cellVoice.cpp#L462-L543) make the same check. Like `sceVoicePausePort` they change nothing that is reported: [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L540-L557) documents that pausing every port keeps the buffered data and the reported state, and nothing here processes port data - [sceVoiceGetMemorySize](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceDeviceInCustomerData](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceQAEnableRecording](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature -- [sceVoiceVADAdjustment](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceVADPVTAdjustment](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceSetMuteFlagAll, sceVoiceSetBitRate, sceVoiceUpdatePort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - not measured on a PS5; signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), behaviour the Vita's [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L250-L557). They return the not-initialized, port-range and missing-port codes the other port calls return. The mute-all flag makes `sceVoiceGetMuteFlag` report every port muted without changing port flags. A non-voice port, a bitrate `sceVoiceCreatePort` rejects, a null param or a different port type throw, since the console's codes for them are unknown - [sceVoiceGetPortAttr](../../../core/libs/prx/libSceVoice/Export.cpp): native initialization, port-range, pointer, attribute-size and port-type validation is implemented. Supported device attributes throw because the host has no device state or voice-activity processing. The former constant-zero voice-activity fallback is removed; failed calls leave output untouched. diff --git a/docs/dev/debt/libSceVoiceChat.md b/docs/dev/debt/libSceVoiceChat.md index 60ad4dc3bb..98557e7eed 100644 --- a/docs/dev/debt/libSceVoiceChat.md +++ b/docs/dev/debt/libSceVoiceChat.md @@ -3,6 +3,7 @@ ### Silent stubs - [sceVoiceChatAbortRequest, sceVoiceChatRegisterChannelEventHandler, sceVoiceChatRegisterDaemonEventHandler, sceVoiceChatRegisterMemberEventHandler, sceVoiceChatRegisterPartyUserStateChangedHandler, sceVoiceChatRegisterVoiceEventHandler, 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) - offline handler registration and event controls return 0 without connecting to a voice chat daemon, because the host environment does not run a PSN voice chat service; titles that register event listeners or start/stop audio capture events proceed safely without throwing +- [sceVoiceChatRegisterHandlers and sceVoiceChatRegisterMicEventHandler](../../../core/libs/prx/libSceVoiceChat/Export.cpp) (libSceVoiceChat) - they keep no handler and return 0 without connecting to a voice chat daemon, so no handler is ever called; the handler table's layout and the handlers' prototypes are not identified. The signatures come from one disassembled title (PPSA21564 01.007.000), not from a console: it calls `sceVoiceChatRegisterHandlers` (0x2e473c) with a 0x30-byte structure of six function addresses and a user argument, then `sceVoiceChatRegisterMicEventHandler` (0x2e4751) with the 32-bit value it also passes as the user id of `sceNpWebApi2CreateUserContext`, a function address and the same user argument, and treats a negative result from either as failure ### Unknown function info diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md index 29b19cc61c..5bb664cbf1 100644 --- a/docs/dev/debt/libSceVrTracker2.md +++ b/docs/dev/debt/libSceVrTracker2.md @@ -2,11 +2,11 @@ ### Silent stubs -- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) never initializes; initialized tracking behavior is absent. Its uninitialized responses were measured on firmware 13.60 without a headset: most exports return 0x8A740001 without writes, while specific output/reset helpers clear buffers or write defaults. These helpers still lack complete signatures. Named export signatures and play-area output layouts are recovered; initialization option semantics and result fields need reconstruction. Anonymous exports remain grouped by their observed response rather than their individual contracts. +- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) never initializes; initialized tracking behavior is absent. Its uninitialized responses were measured without a headset: most exports return 0x8A740001 without writes, while specific output/reset helpers clear buffers or write defaults. These helpers still lack complete signatures. Named export signatures and play-area output layouts are recovered; initialization option semantics and result fields need reconstruction. Anonymous exports remain grouped by their observed response rather than their individual contracts. ### Unknown function info -- [sceVrTracker2Initialize](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - the 72-byte parameter layout is recovered, including thread priorities, CPU affinity masks, work memory, size/alignment and coordinate system. Meanings of the 32-bit option at 0x30 and byte options at 0x38/0x39 remain unresolved; initialization remains unimplemented. +- [sceVrTracker2Initialize](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - never initializes: after validation it returns the first non-zero libSceHmd2 tracking or LED-detector open/start result and resets to uninitialized, and throws if all four succeed or for modes 1000/1001, whose application category the host lacks. Unverified: the mode 0 category query is assumed to succeed, the `sceKernelGetAppInfo2` attribute checks are skipped (the host has no such record; a zeroed one passes), the daemon IPC client and work-memory heap are not created, rollback only undoes what the call opened, and the meanings of the options at 0x30, 0x38 and 0x39 remain unknown (0x39 is only range-checked). The behaviour is not measured on a console; the -1 a no-headset host gets relies on libSceHmd2's measured pre-initialization answers. PPSA05666 calls it with no headset and, with #7463, #7690 and #7658 applied, stopped on the NotImplemented. - [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - the seven anonymous throwing exports now have their consumed argument slots restored. Calibration output (`jRmuXjsknsI`) uses a 32-bit capacity with 608/1216-byte variants; its fields remain unresolved. Mesh requirements (`NBhWy+Gl0tY`) contain four 32-bit counts/sizes. The mode/buffer/capacity/size export (`9R6W33Au4B4`) uses 64-bit sizes. The 32/48-byte result parameters are recovered; result output and pose parameters still need full layouts; the telemetry input (`oZDm1bynCaA`) spans 7024 bytes. Their bodies remain unimplemented. - [Anonymous tracking queries](../../../core/libs/prx/libSceVrTracker2/Export.cpp) still lack initialized behavior. The paired option setter/getter consume 28/40-byte records with three integer options, a reset byte and one/two float vectors; option meanings remain unresolved. The scalar query uses an 8-byte size/float record. Diagnostic output spans 4096 bytes, extended output 0xD02E8 bytes, and tracking configuration input 1024 bytes; their fields remain unresolved. The relative-pose query uses two coordinate-system selectors and a 64-byte output containing a pose and 32 cleared bytes; the counter query uses a 64-bit output and a 32-bit selector whose meaning remains unresolved. Remaining aliases still need individual signatures. diff --git a/docs/dev/debt/libSceWebBrowserDialog.md b/docs/dev/debt/libSceWebBrowserDialog.md index c057cc3710..ba44c2159d 100644 --- a/docs/dev/debt/libSceWebBrowserDialog.md +++ b/docs/dev/debt/libSceWebBrowserDialog.md @@ -3,7 +3,7 @@ ### Silent stubs - [libSceWebBrowserDialog](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) - `sceWebBrowserDialogOpen` and `sceWebBrowserDialogOpenForPredeterminedContent` run the common-dialog state machine without displaying an interactive browser, and `sceWebBrowserDialogGetResult` reports that the user cancelled. Minecraft Bedrock (PPSA17221) imports `sceWebBrowserDialogOpenForPredeterminedContent` (#5744). The 424-byte allow-list parameter and its twenty optional address pointers are validated, but address filtering is not modelled without an interactive browser. Main open-parameter validation remains incomplete. -- [sceWebBrowserDialogSetCookie, sceWebBrowserDialogResetCookie](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) (libSceWebBrowserDialog) - there is no browser cookie store. Both calls check their parameters in the same order and with the same codes as the PS5 12.00 module, then return 0 without storing or clearing anything. On the console they then forward the request to the browser service. While an opened dialog has not been updated to finished, closed or terminated, they return `0x80B80006` (the console's open-dialog flag). +- [sceWebBrowserDialogSetCookie, sceWebBrowserDialogResetCookie](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) (libSceWebBrowserDialog) - there is no browser cookie store. Both calls check their parameters in the same order and with the same codes as the PS5 13.60 module, then return 0 without storing or clearing anything. On the console they then forward the request to the browser service. While an opened dialog has not been updated to finished, closed or terminated, they return `0x80B80006` (the console's open-dialog flag). ### Unknown function info - [sceWebBrowserDialogGetEvent, sceWebBrowserDialogNavigate, sceWebBrowserDialogSetZoom](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) (libSceWebBrowserDialog) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceWebBrowserDialog.c); the no-parameter prototypes follow [shadPS4 `webbrowserdialog.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/web_browser_dialog/webbrowserdialog.cpp#L23-L85) and are not measured on a console. They are declared without parameters solely to resolve known imports; this is not an ABI claim, and every call throws. diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 2d4e0fc01d..ae8a6c9018 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -1,19 +1,24 @@ # libc technical debt +### Silent stubs + +- [_Mtx_current_owns](../../../core/libs/prx/libc/src/Threads.cpp) (libSceLibcInternal, implemented in libc, NID `VYQwFs4CC4Y`) returns 1 and ignores its argument. This follows the PS5 12.00 body reading reported in [PR8859](https://github.com/boykopovar/AnyPS5/pull/8859), rather than inspecting host mutex ownership. The firmware observation is attributed to that contributor; other firmware versions and the argument's console type remain unverified. + ### Unknown function info -- [strtonum](../../../core/libs/prx/libc/src/StringExtensions.cpp) (libc) is unverified on the console. Its signature, decimal parsing, inclusive bounds, error strings, error precedence and errno preservation follow [FreeBSD 11.4 strtonum.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/strtonum.c). Parsing uses the host `strtoll` with base 10. +- [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 - [fegetexcept](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc) has not been read from the PS5 library or measured on a console. It follows FreeBSD 11.4 amd64 [fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h#L121-L133), reporting the six unmasked x87 exceptions independently of MXCSR; whether the PS5 uses the same rule when the units disagree is unverified. - [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. On Windows, unwinding uses `_Unwind_Backtrace` 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. Implemented only on Linux and Windows; 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) validates descriptor access modes as FreeBSD 11.4 [fdopen.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/fdopen.c) does: a requested mode incompatible with a descriptor not opened for both reading and writing returns null with `EINVAL`, leaving the descriptor open. Windows reads the handle's granted access through the same host helper as `fcntl(F_GETFL)`; a closed descriptor returns null with `EBADF`. Console behavior is 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 - [scriptingGetMem, scriptingFreeMem](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - runtime allocator hooks for Unity titles. The allocation signature `(alignment, size)`, minimum alignment 16 and null result for a non-power-of-two alignment follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libKernel.cpp); the free hook's one-pointer signature follows [Raeen's recorded contract](https://github.com/Whoisraeen/Raeen/blob/2db56923204b89719916b95e29c2f3c4e1d9f90b/docs/unity-dlsym-blocker.md). Both use libc's own guest heap, not the title's registered allocator. A title that ships its own `libc.prx` (Metal Gear Solid Master Collection, PPSA18194) has already initialized that allocator, and reaching it from here would run its initializer a second time. That title's IL2CPP runtime looks up only `scriptingGetMem`, by name from `Il2cppUserAssemblies.prx`, and asks it for 256 KiB blocks aligned to 4 KiB. Which console library exports the hooks is unknown, and there is no console measurement. Allocation failure and sizes that overflow return null. -- [_Mtx_trylock](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `k6pGNMwJB08`) - returns busy for a non-recursive mutex already owned by the caller, assumed to match `_Mtx_lock`'s ownership handling; exact console behavior is unverified. -- [nearbyintf](../../../core/libs/prx/libc/src/Math.cpp) (libc, NID `c+4r-T-tEIc`) uses the host rounding mode, not the console's; console rounding mode state remains unverified. +- [_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/Math.cpp) (libc, NID `c+4r-T-tEIc`) rounds with the MXCSR rounding mode, as the console's `libSceLibcInternal.sprx` does in a payload process on a PS5 (firmware 13.60): with the x87 mode set to upward and MXCSR to nearest it rounded 2.5 to 2, and with MXCSR upward to 3, and the four modes gave the results the host gives. The console also raised `FE_INEXACT` for those inexact inputs, which the host's `nearbyintf` does not; whether it does for exact inputs was not measured - [__std_find_trivial_unsized_N](../../../core/libs/prx/libc/src/VectorAlgorithms.cpp) (libc) - matches the `libc.prx` bundled with PPSA08427 (disassembly, and that binary run side by side on random inputs): when nothing matches between `first` and the end of its 32-byte-aligned block, the console rescans that whole block without masking and so can return a match up to 31 bytes before `first` - [_Thrd_sleep](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `jfRI3snge3o`) follows the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`xtime.c`) and C11 Annex K, without console verification. It reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep` on POSIX, retrying on `EINTR`, or high-resolution waitable timers on Windows; a null xtime or one whose normalization overflows throws. Guest thread cancellation is not integrated with this wait. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. +- [setjmp, longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) save the signal mask at 72 in the jump buffer and restore it, as FreeBSD 11.4's [`setjmp` and `longjmp`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/amd64/gen/setjmp.S#L51-L85) do, which is unverified on a console. They go through the `sigprocmask` libkernel registers when it loads and mark the buffer at 88 as `sigsetjmp` does, so a buffer saved before then is jumped to without changing the mask - [_Mtx_init_with_name](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `tgioGpKtmbE`) forwards to `_Mtx_init` and ignores the mutex name parameter. - [_Cnd_timedwait](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `McaImWKXong`) follows the console's `_Cnd_timedwait` (disassembled from a title's `libc.prx`), where xtime is an absolute deadline converted against the current time: it returns `_Thrd_timedout` (2) on expiration and `_Thrd_success` (0) on notification. The console returns 4 for other internal pthread errors while this implementation throws. - [fegetround, fesetround, fetestexcept, feclearexcept, fegetexceptflag and fesetexceptflag](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc) follow FreeBSD 11.4 amd64 [fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): rounding modes use x87 control bits 10-11, exception flags include denormals (`0x3f`), and `fexcept_t` is 16 bits. They access the current host thread's x87 and MXCSR state. Under Rosetta 2 the six MXCSR exception masks always read back set, so an SSE exception the guest unmasks stays masked; the x87 masks and the other MXCSR bits are kept. This console ABI is assumed, as for `fegetenv`, not measured on hardware. Null flag pointers throw instead of faulting. @@ -33,10 +38,12 @@ - [acosl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, like the other libc math functions here, not by the console's (FreeBSD msun ld80 code in Sony's `libc.prx`): results can differ from the console's in the last bit, and on Linux glibc sets `errno` to `EDOM` for `acosl` outside [-1, 1] where msun only raises the floating-point exception. `long double` is the x87 80-bit format, as Sony's `frexpl` uses it (`fldt 0x8(%rsp)`, result in `st(0)`) - [atanl, sinl, cosl, tanl, sinhl, coshl, tanhl, expl, exp2l, logl, log10l and powl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, as `acosl` is, not by the console's FreeBSD msun ld80 code: results can differ from the console's in the last bit (glibc's `log10l(1000)` is not exactly 3), and on Linux glibc sets `errno` to `EDOM` or `ERANGE` where msun only raises the floating-point exception. `ldexpl` and `truncl` are exact operations and give the console's result; `logb` and `remainder` give the console's value too, but on Linux glibc sets `errno` (`ERANGE` for `logb(0)`, `EDOM` for `remainder` by zero or of an infinity) where msun only raises the floating-point exception - [Dinkumware atomics](../../../core/libs/prx/libc/src/DinkumwareAtomics.cpp) (libc) - every operation is sequentially consistent whatever memory order it is given, which is never weaker than the console. Matching the `libc.prx` bundled with PPSA08427 (that binary run side by side on every order from 0 to 7), `_Atomic_load_8` returns 0 for a release, acq_rel or invalid order and the 8-byte exchange and fetch operations return 0 without touching memory for an order above `memory_order_seq_cst`; the generic `_Atomic_copy`, `_Atomic_exchange` and `_Atomic_compare_exchange_*` lock the byte flag passed first, as the console does +- [_Fetch_{and,or,xor}_seq_cst_{1,2,4}](../../../core/libs/prx/libc/src/DinkumwareAtomics.cpp) (libc) follow the two-argument unsigned 8-, 16- and 32-bit helpers in Dinkumware [VC12 xatomic.h](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/xatomic.h), using sequentially consistent atomic read-modify-write operations and returning the original value. The firmware 3.20 export inventory supplies the names and NIDs; the reference defines inline namespace helpers, so the equivalence to the console exports and their calling convention are inferred, not console-measured. A live, suitably aligned object is required as in the reference; invalid-pointer and misalignment fault behavior on the console is unverified. Windows tests cover raw and patched exports, values, neighboring storage and concurrent updates; a finite ordering litmus cannot prove the memory model. - [std_execute_once](../../../core/libs/prx/libc/Export.cpp) (libc) - assumed to be the plain-named twin of `std::_Execute_once` (`_ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1_`, the callee of Dinkumware `call_once`): it delegates there and gets the [thread.once.callonce] semantics already verified for that symbol (one invocation per flag, concurrent callers block until it completes, and a failing or throwing invocation leaves the flag unready so the next caller runs it). The name equivalence is not confirmed on a title, and the callback convention (called with (null, arg, null), non-zero return meaning success) is the twin's, unverified for this symbol - [__atomic_load, __atomic_store, __atomic_exchange and __atomic_compare_exchange](../../../core/libs/prx/libc/src/AtomicBuiltins.cpp) (libc) - the generic, size-parameterised libatomic entry points are a single `ud2` in the `libc.prx` bundled with PPSA08427 (disassembly), so the console traps on any call; these throw instead - [Sized __atomic calls](../../../core/libs/prx/libc/src/DinkumwareAtomics.cpp) (libc) - the 36 `__atomic_load_N`, `__atomic_store_N`, `__atomic_exchange_N`, `__atomic_compare_exchange_N` and `__atomic_fetch_{add,sub,and,or,xor}_N` calls (N = 1, 2, 4, 8) are sequentially consistent whatever order they are given. The `libc.prx` bundled with PPSA08427, run side by side on random values and on every order from -1 to 7, switches on the order but always uses a locked instruction or CAS loop, except that a store with an order other than `memory_order_seq_cst` is a plain store, so these are never weaker than the console. Unlike `_Atomic_load_8` and the 8-byte `_Atomic_*` exchange and fetch calls above, the console accepts every order here, including release, acq_rel and invalid ones, and these do the same - [Dinkumware mutexes and conditions](../../../core/libs/prx/libc/src/Threads.cpp) (libc) use pointer-to-handle arguments, matching the console aerolib signatures reported in [#3079](https://github.com/boykopovar/AnyPS5/pull/3079). The title's `_Mtx_lock` maps `0` to `_Thrd_success` (0), `0x8002000b` to `_Thrd_busy` (3), and other failures to `_Thrd_error` (4); its internal mutex init recognizes recursive flag `0x100`, while every other type uses default attributes. The observed relock failure for a plain mutex suggests those defaults are error-checking, but that has not been confirmed on hardware. The title's `_Mtx_unlock` and `_Cnd_wait` return success regardless of their internal result. The host implementation treats invalid ownership preconditions as no-ops, and throws for invalid handles, host wait errors and destroying a locked mutex as debug guards; those cases are not confirmed against the console. +- [_Mtx_timedlock](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `hPzYSd5Nasc`) follows the console's `libc.prx` at `0x5d50` (PPSA02530): it takes an absolute xtime and returns `_Thrd_timedout` (2) on expiry (`0x8002003c`) or 5 on other failures. On non-Windows hosts the native mutex and condition are `std::timed_mutex` and `std::condition_variable_any`; Windows keeps the `CRITICAL_SECTION`/`CONDITION_VARIABLE` pair because `std::condition_variable_any` failed `guest_libc_cnd_timed_wait` on the Windows runner, and its `_Mtx_timedlock` polls `TryEnterCriticalSection` until the deadline. Windows timing is not measured on the console. - [Floating-point compiler-rt builtins](../../../core/libs/prx/libc/src/FloatBuiltins.cpp) (libc) follow the `libc.prx` bundled with PPSA08427 (disassembly, and that binary run side by side on random inputs) where it differs from current compiler-rt: the soft-float `__add`, `__sub`, `__mul` and `__div` (`sf3`, `df3`) round to nearest, raise no exception flags and ignore MXCSR, here done with SSE under a default MXCSR that is restored afterwards; division flushes a quotient below the smallest normal to a signed zero; `__extendsfdf2` and `__truncdfsf2` copy NaN payloads unshifted; float-to-integer conversions do not saturate but shift the significand by a count masked to the shift width. `__mulsc3`, `__muldc3`, `__divsc3` and `__divdc3` fuse the same products as the console with `fma` and scale through the x87 like its `scalbn`, so their values match, but NaN payloads and exception flags can differ, as the host `fma` and `logb` are not the console's VEX FMA code and libm - [random, srandom, initstate, setstate](../../../core/libs/prx/libc/src/Random.cpp) (libc) - not measured: the state `setstate` returns and its null return for a rear position past the degree, `errno` after `initstate` below 8 bytes, a null state (throws here), an `initstate` seed other than 1, concurrent callers (one lock serialises every call here) and a title's own libc. Measured in a payload process on a PS5 (firmware 12.70): a fresh process draws the `srandom(1)` sequence; `srandom` seeds an additive feedback generator through a `good_rand` that reads its argument as an int32, turns 0 into 123459876 and applies `x = 16807 * (x % 127773) - 2836 * (x / 127773)`, adding 0x7fffffff to a negative result, which reproduces seeds 0, 1, 2, 12345, 0xffffffff and 0x80000000; `initstate` returns null at 0 and 7 bytes and selects types 0 to 4 by size as measured at 8, 15, 31, 32, 63, 64, 127, 128, 129, 255, 256, 257 and 1000 bytes. - [srandomdev](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) is not verified against the console. The firmware 3.20 [export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibcInternal.c) names NID `a2MOZf++Wjg`; it follows FreeBSD 11 [random.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdlib/random.c), filling every active generator word from host OS entropy instead of `KERN_ARND`, then resetting additive feedback positions without warming up. An entropy failure throws rather than leaving `sysctl`'s failure unchecked. Calls share the generator lock. @@ -45,12 +52,12 @@ - [memcpy_s, memmove_s, strcpy_s, strncpy_s, strcat_s, strncat_s, wmemcpy_s, wmemmove_s, wcsncpy_s, wcscat_s, wcsncat_s](../../../core/libs/prx/libc/src/Strings.cpp) (libc) use the C11 Annex K destination size constraints, with RSIZE_MAX taken as (SIZE_MAX >> 1) / sizeof(char16_t) for wide operations and SIZE_MAX >> 1 for byte operations. An oversized destination size returns ERANGE (34) without touching the destination; the console's RSIZE_MAX and exact error value are unverified. Constraint violations return an error without invoking the registered constraint handler, as before. - [wcsncat_s and wcscat_s](../../../core/libs/prx/libc/src/Strings.cpp) (libc) refuse a copy as overlapping only when the written range `[dest + len1, dest + len1 + len2)` meets the read range `[src, src + len2)`. The rule comes from a static reading of the console's `wcsncat_s` in `libc.prx` (not run on a console); `wcscat_s` gets it through `wcsncat_s`, and its own console code was not read. The narrow `strncat_s` keeps a wider rule, `[dest, dest + used + length + 1)` against `[src, src + length + 1)`, whose console code was not read either, so it refuses some copies whose wide equivalent proceeds. - [snwprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: the handler installed with `set_constraint_handler_s` is not invoked, and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` -- [swprintf_s and vswprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follow C11 K.3.9.1.4 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`: the complete output, including the terminating null, must fit in the buffer, otherwise it is a runtime-constraint violation that sets `buffer[0]` to zero and returns a negative value, and the buffer is not partially written. Argument and encoding checks match `snwprintf_s` -- [strerror](../../../core/libs/prx/libc/src/Errors.cpp) (libc) - follows the `libc.prx` that CUSA04865 and CUSA12435 ship, loaded on a PS4 Pro (firmware 10.71): 0 gives "No error", 1 to 94 the FreeBSD texts, any other number "error_code=0x%08x", and errno is left alone. The console's system libc, which homebrew without a bundled `libc.prx` uses, answers differently ("No error information" for 0 and unknown numbers, 95 and 96 named). `strerror_r` and `perror` keep "No error: 0" and "Unknown error: %d"; they are not measured. -- [strerror_s and strerrorlen_s](../../../core/libs/prx/libc/src/WideConversion.cpp) (libc) follow [C11 K.3.7.4.2 and K.3.7.4.3](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf), using the existing FreeBSD error messages and unknown-error formatting. Truncation writes an ellipsis when capacity exceeds 3; constraint failures leave the destination untouched and invoke the registered guest-ABI handler. The console's `RSIZE_MAX` (`SIZE_MAX >> 1` here), nonzero returns (`EINVAL` 22 for constraints, `ERANGE` 34 for truncation), handler arguments and default abort behavior are unverified. Messages remain in the C locale, and guest errno is preserved. -- [getw and putw](../../../core/libs/prx/libc/src/BinaryWordIo.cpp) (libc) follow FreeBSD 11.4 [getw.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/getw.c) and [putw.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/putw.c): one 32-bit little-endian word, `getw` returns -1 for EOF or an error, and `putw` returns 0 on success or -1 on error while holding the stream lock. They use the existing host-backed streams, so buffering and error timing follow the host CRT. Null streams throw and closed streams return -1 with `EBADF`, as existing byte I/O does. Console behavior is unverified. +- [swprintf_s and vswprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follow C11 K.3.9.1.4 with `RSIZE_MAX` assumed to be `(SIZE_MAX >> 1) / sizeof(char16_t)`: the complete output, including the terminating null, must fit in the buffer, otherwise it is a runtime-constraint violation that sets `buffer[0]` to zero and returns a negative value, and the buffer is not partially written. Argument and encoding checks match `snwprintf_s` +- [strerror, strerror_r and perror](../../../core/libs/prx/libc/src/Errors.cpp) (libc) - match the system `libSceLibcInternal` of PS5 firmware 13.60 and the `libc.prx` that CUSA04865 and CUSA12435 ship, loaded on a PS4 Pro (firmware 10.71): 0 gives "No error", 1 to 94 the FreeBSD texts and any other number "error_code=0x%08x"; `perror` prints the same texts; errno is left alone. `strerror_r` follows the `libc.prx` that PPSA31872 ships, read statically: a buffer under 23 bytes returns `ERANGE` (34) without writing, otherwise it `strncpy`s length - 1 bytes, terminates the last byte and returns 0, so a long text is cut. Firmware 13.60 agreed at 0, 1, 5, 16 and 256 bytes; 17 to 255 bytes and the cut were not run on a console. A null buffer of 23 bytes or more throws, as the console writes through it. +- [strerror_s and strerrorlen_s](../../../core/libs/prx/libc/src/WideConversion.cpp) (libc) - a null destination, a capacity of 0 or one above `RSIZE_MAX` (`SIZE_MAX >> 1` here) calls the registered guest-ABI constraint handler with `EINVAL` and leaves the destination untouched, following [C11 K.3.7.4.2](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf); what the console does then, its `RSIZE_MAX` and its default handler are not measured. Otherwise they match the system `libSceLibcInternal` of PS5 firmware 13.60: the `strerror` texts, a short buffer truncated with "..." when it holds more than 3 bytes and a return of 0 either way, and `errno` left alone +- [getw and putw](../../../core/libs/prx/libc/src/BinaryWordIo.cpp) (libc) follow FreeBSD 11.4 [getw.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/getw.c) and [putw.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/putw.c): one 32-bit little-endian word, `getw` returns -1 for EOF or an error, and `putw` returns 0 on success or -1 on error while holding the stream lock. They use the existing host-backed streams, so buffering and error timing follow the host CRT. Null streams throw and closed streams return -1 with `EBADF`, as existing byte I/O does. Console behavior is unverified. A payload process on a PS5 (firmware 13.60) gave the same results: two words written little-endian, -1 with end of file after them and for a 2-byte file, and -1 with the error flag for `putw` on a read-only stream - [vsnwprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - `va_list` counterpart of `snwprintf_s` (C11 K.3.9.1.3): truncates to the buffer and returns the full length, with the same constraints and argument checks -- [strndup and strdup](../../../core/libs/prx/libc/src/AllocatingStrings.cpp) (libc) return null with `errno` set to `EINVAL` (22) when `source` is null, preventing a host segfault on invalid guest input. POSIX.1-2008 and FreeBSD 11.4 libc leave `strndup(NULL)` and `strdup(NULL)` undefined (faulting); exact console behavior on hardware is unverified. +- [strndup and strdup](../../../core/libs/prx/libc/src/AllocatingStrings.cpp) (libc) return null with `errno` set to `EINVAL` (22) when `source` is null, preventing a host segfault on invalid guest input. POSIX.1-2008 and FreeBSD 11.4 libc leave `strndup(NULL)` and `strdup(NULL)` undefined (faulting). The `libc.prx` that titles ship has no null check either (read from disassembly of PPSA13934's copy, not measured on a console): `strdup` (0x6b440) calls `strlen` and `strndup` (0x6b650) calls `strnlen` on the source before allocating, so both fault on a null source there, except `strndup(NULL, 0)`, whose `strnlen` reads nothing and which returns an empty copy; here it returns null with `EINVAL`. - [snprintf_s and vsnprintf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - follow C11 K.3.5.3.5 and K.3.5.3.12 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`, as `snwprintf_s` does: a null buffer or format, `size` of 0 or above `RSIZE_MAX`, any `%n` and a null `%s`, `%ls` or `%S` argument return a negative value and set `buffer[0]` to zero when the buffer and size allow it; otherwise the output is truncated to the buffer and the full length is returned. A violation only returns: the constraint handler is not invoked, and the console's error value is unverified. Not confirmed on a PS5 title - [vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - returns the count of items converted before an undersized `%s`/`%c`/`%[` destination (0 when it is the first), not EOF. Not confirmed on a PS5 title - [setlocale](../../../core/libs/prx/libc/src/StdioExtras.cpp) (libc) knows only the "C" locale, as the `libc.prx` that titles ship does: it ignores the category, returns "C" for a null, empty or "C" name and a null pointer for any other, "POSIX" included, without setting `errno`. Checked by running the copies bundled with PPSA07923 and PPSA13934 side by side for categories -40 to 79 and 2^30, not measured on a console. @@ -68,20 +75,21 @@ - [_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`) returns the host's `std::thread::hardware_concurrency()` (which may be 0 when unknown) instead of the console's processor count; no reference implements it and the value a title expects (PPSA34015 imports it) is unverified. - [fmodf](../../../core/libs/prx/libc/src/Math.cpp) (libc) - console `errno`, exception flags and NaN payloads remain unverified. The wrapper preserves `errno` like the existing double `fmod`, following FreeBSD 11.4 [`e_fmodf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_fmodf.c); host `std::fmod` still supplies the result and exception flags. -- [_FSinh and _FCosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) finite results are not verified bit for bit against the console: the scaled product uses the host's binary64 hyperbolic function before rounding to binary32, rather than the float rational and `_FExp` sequence in PRAGMATA's bundled `libc.prx`. NaN preference and payloads, signed zeros, infinite operands and the range-error thresholds follow that binary's instructions; the system `libSceLibcInternal` implementation and nondefault flush-to-zero modes are unverified. +- [_LCosh and _LSinh](../../../core/libs/prx/libc/src/Math.cpp) (libc) follow the two-argument Dinkumware CRT helpers `y * cosh(x)` and `y * sinh(x)`, including scaled large-finite-input paths; the signatures are confirmed by Dinkumware's public `ymath.h`, but the exact console results, range-error thresholds and floating-point exception behavior are not verified. +- [_FSinh and _FCosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) finite results are not verified bit for bit against the console: the scaled product uses the host's binary64 hyperbolic function before rounding to binary32, rather than the float rational and `_FExp` sequence in PRAGMATA's bundled `libc.prx`. NaN preference and payloads, signed zeros, infinite operands and the range-error thresholds follow that binary's instructions; the system `libSceLibcInternal` implementation and nondefault flush-to-zero modes are unverified. Against `libSceLibcInternal.sprx` on a PS5 (firmware 13.60), 12 of 36 measured results differ by one unit in the last place (`_FSinh(0.5, y)` and `_FCosh(1, y)`, `_FCosh(10, y)` for y of 0.5, 1 and 2); zeros, the overflow to infinity at 89 and 90 and the tiny-argument results matched +- [_Divide, _FDivide, _Recip and _FRecip](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libc) follow the Dinkumware CRT division and reciprocal contracts with IEEE-754 signed-zero, infinity and NaN behavior. The exact console results, signaling-NaN handling and floating-point exception behavior are not verified. - [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. -- [memmem and memccpy](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `memmem` searches for needle occurrences in memory buffers and `memccpy` copies buffers until a character match, following FreeBSD and POSIX specifications; behavior is unverified on console. +- [memmem and memccpy](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `memmem` searches for needle occurrences in memory buffers and `memccpy` copies buffers until a character match, following FreeBSD and POSIX specifications; behavior is unverified on console. Neither is exported by `libSceLibcInternal.sprx` on a PS5 (firmware 13.60), so a payload process cannot call them there. - [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) round to the nearest float, where the console's `_Stof` in `libSceLibcInternal.sprx` (0x734a0) does not always: about a third of decimal inputs within a relative 2^-51 of a point halfway between two floats, and some hexadecimal inputs with more than 24 significant bits, come out one unit apart in the last place (`1e-40` is `0x1.16c3p-133` there and `0x1.16c2p-133` here). The console's own rounding is not reproduced - [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `ERANGE` follows the `libc.prx` bundled with PPSA08427 (J03547016) and PPSA03671 (J03848509), both run side by side with this libc on a Linux host: a decimal input whose result is a nonzero subnormal sets it only when inexact, a hexadecimal one only when it lies below the smallest subnormal and rounds up to it, and underflow to zero and overflow set it. Both builds also take a `NAN(...)` payload in `strtof` from the sequence's leading digits (`nan(0x123)` is `0x7fc00123`, `-nan(5)` is `0xffc00005`); the sign is kept here, the payload is not. `libSceLibcInternal`'s `_Stof` differs: no `ERANGE` for hexadecimal input, a positive `nan`, and after a minus sign a failed parse is `-0.0` for a lone sign and `+0.0` before an `inf`/`nan` spelling, the reverse of `libc.prx`. Decimal `strtod` sets `ERANGE` for a nonzero subnormal in J03547016 but not in J03848509 or `libSceLibcInternal`, whose result is kept. -- [Guest time conversions](../../../core/libs/prx/libc/src/Time.cpp) rewrite the host process `TZ` when a guest changes its own `TZ`, so other host consumers of that variable (including libSceRtc local time) shift too; code outside libc does not take the zone lock. Zone syntax follows the host C runtime, not FreeBSD: Windows only accepts the `std offset [dst]` form, treats an empty guest `TZ` as `UTC0` and reads `:Europe/Berlin` as an abbreviation (for example `rop`); only Linux honours zone file names. If the guest accepts a `TZ` value that the host rejects (on Windows, for example, one longer than the host environment limit), conversions throw `std::runtime_error`. A `tm_zone` saved before a `TZ` change can refer to storage now holding the new zone name, changing `%Z` output. +- [Guest time conversions](../../../core/libs/prx/libc/src/Time.cpp) rewrite the host process `TZ` when a guest changes its own `TZ`, so other host consumers of that variable (including libSceRtc local time) shift too; code outside libc does not take the zone lock. Zone syntax follows the host C runtime, not FreeBSD: Windows only accepts the `std offset [dst]` form, treats an empty guest `TZ` as `UTC0` and reads `:Europe/Berlin` as an abbreviation (for example `rop`); only Linux honours zone file names. If the guest accepts a `TZ` value that the host rejects (on Windows, for example, one longer than the host environment limit), conversions throw `std::runtime_error`. A `tm_zone` saved before a `TZ` change can refer to storage now holding the new zone name; `strftime` does not read it. - [cbrtf](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`s_cbrtf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_cbrtf.c): a 5-bit first guess from the exponent bits, then two Newton steps in double precision. It sets no `errno`, as the `libc.prx` that titles ship does not. Floating-point exceptions are not compared. It gives the same bits as the `cbrtf` of the `libc.prx` bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host. -- [__isfinitel](../../../core/libs/prx/libc/src/Math.cpp) - classifies the x87 80-bit exponent without narrowing to double or performing floating-point comparisons, following [FreeBSD msun](https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_isfinite.c). The signature and classification follow FreeBSD; console behaviour is unmeasured. Requires little-endian x87 long double stored in 16 bytes, as the existing guest long-double ABI does. +- [__isfinitel](../../../core/libs/prx/libc/src/Math.cpp) - classifies the x87 80-bit exponent without narrowing to double or performing floating-point comparisons, following [FreeBSD msun](https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_isfinite.c). The signature and classification follow FreeBSD; console behaviour is unmeasured. Requires little-endian x87 long double stored in 16 bytes, as the existing guest long-double ABI does. On a PS5 (firmware 13.60) `__isfinitel` returned 1 for 0, -0, 1, a denormal and ±1e4000, and 0 for a NaN and for the maximum exponent without the integer bit, which this classification gives too - [logf](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`e_logf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_logf.c), with `vzero` read at run time as msun's `volatile` read does. As in the `libc.prx` that titles ship, `errno` is set to `ERANGE` for ±0 and to `EDOM` for every argument with the sign bit set, negative NaNs and -inf included; positive NaNs and +inf leave it unchanged. Floating-point exceptions are not compared. It gives the same bits and `errno` as the `logf` of the `libc.prx` bundled with PPSA07923 and PPSA05916 for all 2^32 arguments, run on a Windows host. The FMA-contracted build bundled with PPSA13934 differs from it by 1 ulp on 961,881 arguments, so no single port matches every title's last bit. - Windows direct-memory backing reserves a paging-file section. A [direct memory page](../../../core/libs/prx/libc/include/WindowsMappings.hpp) mapped at one guest address is private write-watched memory at that address and is not committed in the section; a page mapped at a second address moves its whole resident run into the section (committed there) and every address becomes a view of it, tracked by write protection. An unmapped resident page that is not all zeros is kept in a private reservation until it is mapped again. Committed section contents remain across unmap/remap and are released when the backing and its remaining views are destroyed. A host import alias over part of a resident run is refused. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified. -- Stable Windows resident runs of at least 256 KiB use temporary read-only protection for clean write-watch proofs. The first CPU write restores the logical protection of the whole run; that run remains on native write watching until it is remapped, so repeatedly rewritten runs stay writable. Native kernel writes into guest buffers must hold [GuestArena::HostWrite](../../../core/libs/prx/libc/include/GuestArena.hpp), as shared views already require: an unbracketed WinAPI ReadFile can fail with ERROR_NOACCESS while the resident proof is armed. Guest read, pread and socket receive paths already bracket their destinations. GPU host-import access suppresses resident rearming without inventing shared-page stores. This is a host integration requirement, not a change to guest logical protection. - [getc_unlocked, putc_unlocked, getchar_unlocked, putchar_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. - [strcasecmp and strncasecmp](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - console locale behavior is unmeasured. They use the guest's C-locale case conversion and return -1, 0 or 1, the sign of the folded unsigned-byte difference, as the `libc.prx` bundled with eleven titles does (FreeBSD 11.4's [strcasecmp.c](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/string/strcasecmp.c) returns the difference itself); other guest locales are not implemented. - [feof_unlocked and ferror_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) have no console measurement. The firmware 3.20 [export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibcInternal.c) names NIDs `NuydofHcR1w` and `yxbGzBQC5xA`. They follow FreeBSD 11 [feof.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/stdio/feof.c) and [ferror.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/stdio/ferror.c), returning 0 or 1 from the stream status without acquiring the stream lock. They inherit the host CRT limitations of the unlocked byte operations above. @@ -98,19 +106,25 @@ - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) - [_Stod, _Stodx, _Stofx, _Stoldx, _Stof and _Stold](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libSceLibcInternal, implemented in libc) throw for hexadecimal text with a non-zero power-of-ten scale. The console's result there does not follow a simple rule: `0x3` with a scale of -1 gives 0.29999999999999999. A nonzero subnormal result sets neither `errno` nor the error argument, as measured in firmware 13.60's `libSceLibcInternal.sprx` run in a Linux host process, not on a console; there about a fifth of decimal inputs with a subnormal result come out one unit apart in the last place (`_Stodx("2.2250738585072014e-308", 0)` is the largest subnormal), which is not reproduced +- [_LDivide, _LRecip and _LLog](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libc) follow the public Dinkumware CRT contracts: long-double division and reciprocal, and the `_LLog` base flag (`0` natural, positive base 10, negative base 2). The exact console results, signaling-NaN behavior and floating-point exception behavior are not verified. - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console - [atanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`s_atanf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_atanf.c), with `atanlo[3]` read at run time as msun's `volatile` does. Tiny arguments return `x` without raising `FE_INEXACT`. It gives the same bits as the `atanf` of the `libc.prx` bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host, and none of those sets `errno`. Whether other titles' builds differ is unverified. -- [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `rmdir("dir/.")`, `unlink("dir/.")` and the same calls with `..` follow the same lookup rule in FreeBSD but are not changed here: the guest still resolves the dot component away and acts on `dir` or its parent. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. -- [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. +- [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `unlink`, `rmdir`, `sceKernelUnlink` and `sceKernelRmdir` follow the same rule. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. +- [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) imports nanf. - [jrand48 and mrand48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) return the high 32 bits of the new 48-bit state zero-extended to 64 bits, as the console's `libSceLibcInternal.sprx` does when called from a homebrew payload process on a PS5 (firmware 12.70): a result with bit 31 set comes back between 0x80000000 and 0xFFFFFFFF, as FreeBSD 11.0's [`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/jrand48.c#L23) and [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/mrand48.c#L25) return it; FreeBSD 11.1 and later cast it to `int32_t` ([`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/jrand48.c#L26), [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/mrand48.c#L28)), which makes it negative. `lrand48` and `nrand48` matched FreeBSD there. Whether a title's own libc does the same is unverified - [seed48 and lcong48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x6F460 and 0x6F420): the multiplier and addend are shared by the whole `rand48` family, including `erand48`, `nrand48` and `jrand48` on a caller's seed; `lcong48` sets them with the seed, and `srand48` and `seed48` restore 0x5DEECE66D and 0xB. Unlike FreeBSD, `seed48` returns a static copy of the new seed rather than the previous one. A null argument, which the console dereferences, throws +- [drand48 and erand48](../../../core/libs/prx/libc/src/Random.cpp) (libc) return the upper 32 bits of the new 48-bit state over 2^32, as the `libc.prx` bundled with eleven titles does on 10,000 values per function; FreeBSD returns all 48 bits. Whether the console's `libSceLibcInternal.sprx` copy does the same is unmeasured. - [lgamma and signgam](../../../core/libs/prx/libc/src/Math.cpp) (libc) - `lgamma` returns the host's `lgamma` and stores the sign of Γ(x) in the exported `signgam` (NID `5gOOC0kzW0c`, starting at 0 as in FreeBSD 11's [`s_signgam.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/s_signgam.c)) by the rules of [`e_lgamma_r.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma_r.c): -1 for -0 and for a negative non-integer whose floor is odd, 1 otherwise, including poles, infinities and NaN. Taken from the FreeBSD source, not measured on the console. The NID is in the aerolib symbol list; no title is known to read `signgam`. PPSA99004 imports `lgamma` - [coshf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows FreeBSD msun's [`e_coshf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_coshf.c), using the `expm1f` and `expf` paths already inlined for `sinhf` below; the Windows UCRT differed from the `libc.prx` copies in PPSA07923 and PPSA13934 on 500,554 and 500,614 of 20 million inputs in the #9707 survey. The exact system `libSceLibcInternal` behavior and the FMA-contracted `libc.prx` results remain unverified - [expm1](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is FreeBSD msun's [`s_expm1.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_expm1.c) with `huge` read at run time, and sets `ERANGE` only when a finite argument overflows to infinity, as the titles' `libc.prx` does. It gives the same bits and `errno` as the `expm1` bundled with PPSA07923 and PPSA05916 on 320 million random and 47,222 structured arguments on a Windows host; the FMA-contracted build bundled with PPSA13934 differs on 43,433 of the random ones. +- [log1p](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is FreeBSD msun's [`s_log1p.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_log1p.c). As the titles' `libc.prx` does, it sets `EDOM` at msun's NaN return (arguments below -1, `-inf` and negative NaNs) and leaves `errno` alone for `log1p(-1)`, where it returns `-inf`. It gives the same bits, NaN payloads included, and `errno` as the `log1p` bundled with PPSA07923 and PPSA05916 on 320 million random and 68,216 structured arguments on a Windows host. The FMA-contracted build bundled with PPSA13934 differs on 73,682 of the random ones. +- [nextafter and nextafterf](../../../core/libs/prx/libc/src/Math.cpp) (libc) have no physical-console or unmasked-exception validation. A Linux CPU probe compares result bits, `errno` and masked floating-point flags with the unchanged math code in FAR: Changing Tides PPSA03766 bundled `libc.prx` (`nextafter` 0x51b30, `nextafterf` 0x51c80, `_Feraise` 0x11da0): 16,864 calls across both precisions, four rounding modes and all DAZ/FTZ combinations, not exhaustive inputs. Earlier Windows sampling of eleven bundled libraries compares only bits and `errno`; exception behavior outside the FAR module remains unverified. +- [tanh](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is FreeBSD msun's [`s_tanh.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_tanh.c) over this file's msun `Expm1`, with `tiny` read at run time; it sets no `errno`, as the titles' `libc.prx` does not. It gives the same bits as the `tanh` bundled with PPSA07923 and PPSA05916 on 16 million random and 68,211 structured arguments on a Windows host; the FMA-contracted build bundled with PPSA13934 differs on 1,054 of them. - [sinhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed -- [acoshf](../../../core/libs/prx/libc/src/Math.cpp) (libc) uses FreeBSD msun's [`e_acoshf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_acoshf.c) range handling; the Windows UCRT differed from the `libc.prx` copies in PPSA07923 and PPSA13934 on 216,017 and 216,024 of 20 million inputs in the #9707 survey. The invalid-domain path raises `FE_INVALID` without setting `errno` as the source does. The exact system `libSceLibcInternal` behavior and FMA-contracted results remain unverified +- [acoshf](../../../core/libs/prx/libc/src/Math.cpp) (libc) exact system `libSceLibcInternal` behavior and FMA-contracted copies remain unverified. Positive finite inputs use [msun](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_acoshf.c) range handling and this file's `logf`; the middle range divides -1 before addition under directed rounding. Bits, errno and SSE/x87 status and controls match the supplied PPSA03766/PPSA07923 copies on 236,592 positive finite calls (4,929 inputs, four rounding modes, DAZ/FTZ combinations and sticky flags); these samples are not exhaustive. Invalid-domain errno and SSE/x87 invalid flags are checked against two supplied bundled copies; those samples are not exhaustive either. - [acoshl, asinhl, atanhl, atan2l, cbrtl, erfl, erfcl, expm1l, hypotl, log1pl, log2l, lgammal, tgammal, j0f, j1f, jnf, y0f, y1f and ynf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return the host C library's results (the float Bessel functions through its double ones), not those of the console's own code, so the last bits can differ (`y0f`, `y1f`, `ynf` follow the console at `+-0`). As in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x56410), `lgammal` does not store the sign in `signgam`; `hypotl` returns infinity for an infinite argument even when the other is a NaN, as C11 F.10.4.3 requires. [hypot3, hypot3f and hypot3l](../../../core/libs/prx/libc/src/Math.cpp) follow that module (0x51D30, 0x520D0, 0x52420): the first NaN of x, y and z is returned unchanged, any infinity gives positive infinity, a zero argument gives the two-argument hypot of the other two, and otherwise the largest magnitude is combined with the hypot of the other two - [asinl](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code +- [acosf](../../../core/libs/prx/libc/src/Math.cpp) (libc) - for |x| > 1 and NaN the console stores `EDOM` and raises `FE_INVALID` before returning `(x - x) / (x - x)`; here those return the same NaNs and leave `errno` as it was (#3688). For |x| <= 1 it follows the `acosf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_acosf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_acosf.c), with the same constants, `pio2_lo` read at run time and the `0xfffff000` mask on `sqrtf(z)`, which the binary holds. Titles ship different builds of that `libc.prx`: PPSA13934's is FMA-contracted and differs from this one by 1 ulp on 129,170 inputs (#9668), so no single port matches every title's last bit. - [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 - [j0, j1, jn, y0, y1 and yn](../../../core/libs/prx/libc/src/Bessel.cpp) (libc) call the host's `sin`, `cos`, `sincos` and `log`, so the last bits can differ from the console. The rest follows the system `libc.prx` built as `J03093718` (disassembly), which given the console's own `sin`, `cos`, `sincos` and `log` gave the same bits and exception flags for 90 million arguments: msun's [`e_j0.c`, `e_j1.c` and `e_jn.c`](https://github.com/freebsd/freebsd-src/tree/release/10.0.0/lib/msun/src) with each multiply-add the console fuses done as one FMA. As in that release, before FreeBSD commit `186f620727`, `y0`, `y1` and `yn` return `-inf` for ±0 and `0x7ff8000000000000` for a negative argument without raising an exception. The order wraps at 32 bits as on the console: `jn(1 << 30, x)` never returns for 2^-29 <= |x| < 2^30 - [getenv, setenv, unsetenv, putenv](../../../core/libs/prx/libc/src/Environment.cpp) (libc) keep one guest environment that starts empty, like the `environ` libkernel exports, and never reads or changes the host's. A title sees only the variables it sets itself; host variables such as `HOME` and `PATH` name host paths a guest cannot use (PS5MC, PPSA99420, exits with `XBMC_Run returned -1` when it gets the host's `HOME`). Whether the console starts a title with any variables set is not measured @@ -131,39 +145,53 @@ - The Windows libc [exception unwinder](../../../core/libs/prx/libc/src/exception/Unwind.cpp) cannot run arbitrary Windows exception or termination handlers. It accepts GCC C and C++ handlers identified by a project's `.sehgcc` section and interprets their LSDA with the guest personalities. A handler in an unmarked module, such as a C++ cleanup through `__gxx_personality_seh0` in `libstdc++-6.dll` or a system DLL function like `qsort` in `ucrtbase.dll` (Windows 10 22H2), still stops unwinding and the exception is reported as unhandled. SjLj exceptions are unsupported; Scoop GCC 15.2.0 selects them when SEH is disabled and lacks the System V ABI SEH compiler fix carried by MSYS2. - The libc [SSE4a trap emulation](../../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. - [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists host interfaces and IPv4/IPv6 addresses in FreeBSD's layouts. Windows and macOS omit per-interface AF_LINK entries and interfaces without an address; on Windows the flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST` (PPP/tunnel adapters omit broadcast and point-to-point, and an unspecified prefix length gets a host mask). On Linux, `AF_LINK` `if_data` maps non-Ethernet/loopback/PPP types to `IFT_OTHER` (1) and leaves `ifi_omcasts`, `ifi_noproto`, `ifi_hwassist`, `ifi_metric`, `ifi_epoch`, `ifi_lastchange`, unknown baud rates and address-entry `ifa_data` as 0/null; a point-to-point address without a peer loses `IFF_POINTOPOINT`, and an omitted IPv4 broadcast is derived from the netmask. Interface names are the host's (`if` on Windows above 15 bytes). -- [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. +- [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. - [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 - [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) - a null state, which the console dereferences, throws. Otherwise they follow the console's `libSceLibcInternal.sprx` (0x7D480 to 0x7D920), read from disassembly: UTF-8 whatever the locale, up to six bytes per sequence, with no surrogate or 0x10FFFF check when encoding. The state holds the value in its first eight bytes and the expected byte count (0x10 for a pending low surrogate, 1 for a stored high one) as 16 bits at 0xA. `mbrtoc16` builds a low surrogate from the value the state last held, `c16rtomb` returns -1 without `errno` for an unpaired high surrogate, and `EILSEQ` is 86. On a PS5 (13.60) the system library's default locale converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen`; a title's bundled `libc.prx` does too until `setlocale` installs a table ([disassembly](https://github.com/boykopovar/AnyPS5/pull/8855#issuecomment-6335267758)) -- [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) follows a payload process on a PS5 (firmware 13.60): each 16-bit unit of a `%ls` or `%lc` argument is encoded alone, so a surrogate pair gives two 3-byte sequences; a NaN keeps its sign and the `+` and space flags, and `0` pads it with spaces; argument positions above 99 work (up to 400 measured; above 65535 throw here); an unknown conversion such as `%m` or `%k` prints `%` and its letter, padded to the width, without taking an argument. The guest's `wcrtomb` and locales are not used. On Linux the formats without those conversions go to glibc, which prints NaN the same way. The `_s` forms throw for an unknown conversion, `%m` included, which was not measured +- [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) follows a payload process on a PS5 (firmware 13.60): each 16-bit unit of a `%ls` or `%lc` argument is encoded alone, so a surrogate pair gives two 3-byte sequences; a NaN keeps its sign and the `+` and space flags, and `0` pads it with spaces; argument positions above 99 work (up to 400 measured; above 65535 throw here); an unknown conversion such as `%m` or `%k` prints `%` and its letter, padded to the width, without taking an argument. The guest's `wcrtomb` and locales are not used. On Linux the formats without those conversions or a floating-point conversion go to glibc, which prints NaN the same way. The `_s` forms throw for an unknown conversion, `%m` included, which was not measured +- [%a and %A](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) format a finite double as the `libc.prx` that titles ship does, instead of through the host: a subnormal is normalised (`0x1p-1074`), a shorter precision rounds half up on the dropped hexadecimal digits and a carry stays in the leading digit (`%.3a` of `0x0.fffffffffffffp-1022` is `0x2.000p-1023`), and `-` overrides `0`. Checked by running the copies of PPSA13934, PPSA07923 and PPSA05916 side by side on 7 million random formats, not measured on a console. A `long double` is normalised the same way, with 16 hexadecimal digits (`0x1.999999999999999ap-4` for 0.1L). Infinities still go to the host; NaN keeps its sign and flags through the formatter on current main. +- [%e, %f and %g](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) format a finite `double` or `long double` as the `libc.prx` that titles ship does, instead of through the host. It keeps the exact decimal digits down to position `4 * floor(k / 4) - 20`, where `k` is the `frexp` exponent times 30103 / 100000 truncated, and prints zeros after them; a shorter precision rounds half up on the kept digits, except that an exact tie rounds down when `k` is at least 8 (`%.0f` of 67108865.5 is `67108865`). The console computes the kept digits in extended precision, so about 7% of outputs longer than 17 significant digits end one unit lower than here (`%f` of 1e22 is `9999999999999999999999.000000`). Checked by running the copies of PPSA07923, PPSA05916 and PPSA13934 side by side, not measured on a console. The wide printf family uses the same digits, as the titles' `swprintf` gives the same text as their `snprintf`. - [%p and %lp](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) follow the console's `libSceLibcInternal.sprx` measured from a homebrew payload on firmware 12.70 ([#7549](https://github.com/boykopovar/AnyPS5/issues/7549)), not FreeBSD: bare lowercase hex with no `0x` prefix even under `#`, `0` for a null pointer, `+` and the space flag ignored, the width counting only the digits, `-` padding on the right, `0` padding with zeros when there is no precision, the precision as the minimum number of digits, nothing for a null pointer under `%.0p`, and `l` accepted. Only `snprintf` and `swprintf` of that module were measured, from a payload process rather than a title process, so a width beside `%.0p`, the `0` flag beside a precision (which C17 ignores) and `%.0p` of a non-null pointer follow this formatter's own rules and are not measured; `swprintf` was not run with `%lp`, so the wide family still refuses that conversion and returns -1. Linux routes `%p` and `%lp` to this formatter because glibc prints the FreeBSD form and `(nil)` for a null pointer, which means a format that combines `%p` with a conversion this formatter refuses, such as an argument position above 99, throws there instead of reaching glibc - [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. `codecvt` reproduces the console virtuals (`noconv` conversions, `length` the shorter range); `numpunct` copies its C-locale table (`""`, `.`, `,`, `false`, `true`). `num_put`/`time_put` formatting virtuals throw rather than fabricate output, and `time_put`'s `_Locinfo` table is unpopulated — the console's `do_put` bodies were not mirrored. The facets share the no-op `destroy`/`deleteObject`/`retain`/`release` base used for `collate`, so `numpunct`'s static C-locale strings are never freed. Unlike `collate`, these `_Getcat` entries ignore the locale argument and always build the C-locale facet; the console's behaviour for a non-C name is unverified. `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::__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`, and fail to load on main with unresolved import (0xC0000139). +- [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`. - [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/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` +- [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) on Linux reads a `q` length as `ll`, as glibc does. On Windows a `q` directive ends the scan with nothing stored, as the console's own libc does for `%qd`; FreeBSD 11.4's source reads it as `ll`. - [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. - [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. The console's names are not known +- [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. -- [strftime and asctime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow FreeBSD 11 [strftime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libc/stdtime/strftime.c) and [asctime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/contrib/tzcode/stdtime/asctime.c) with the C locale on every host, not measured on the console; other locales are not implemented. `%s` follows FreeBSD 9 and 11 and is the local-time `mktime` of the broken-down time (current FreeBSD uses `tm_gmtoff`), which is -1 on Windows outside the range above. `%Z` is `tm_zone`, or `tzname[tm_isdst != 0]` when it is null and `tm_isdst` is not negative. The two-digit year warning FreeBSD writes to `stderr` when `CHECK_STRFTIME_FORMATS_FOR_TWO_DIGIT_YEARS` is set is not written +- [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. `%G`, `%g` and `%V` throw for a negative `tm_yday`, a `tm_yday` past the year or a `tm_wday` below -6, where no rule fits the console: `%V` is 01, 00, 00 and 52 for `tm_yday` -25, -13, -12 and -1 (`tm_wday` 5), `tm_yday` 400 of 2026 is week 01 of 2027 but 415 of 1997 is week 59 and 365 of 2013 week 53, and `tm_wday` -12 with `tm_yday` 18 is week 04. `%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) converts the format to UTF-8, runs this library's `strftime` and converts the result back, where the console's `libSceLibcInternal.sprx` (0x854e0) copies literal wide characters and expands each conversion straight into the buffer; the results match for the formats tried, and it has the `strftime` gaps above. A result that does not fit returns 0 with the buffer untouched, where the console leaves the characters written so far. A null buffer returns 0, where the console writes through it, and a size too large for the narrow buffer returns 0 with `EINVAL`, where the console formats as usual. A null format or `tm` throws - [syslog](../../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER` or the facility set by `openlog`, and errno is preserved. `openlog` applies its identity and `LOG_PID`; a null identity retains the previous one, and a valid nonzero facility replaces the default. `closelog` clears the identity but retains the options and facility, as in FreeBSD 11; when no identity is set, FreeBSD falls back to the program name, which this backend does not emit. `LOG_PERROR` has no extra effect because stderr is the emulated log sink; `LOG_NDELAY`/`LOG_ODELAY`, `LOG_NOWAIT` and `LOG_CONS` have no daemon, child or console backend to control. `setlogmask` follows [FreeBSD 11](https://github.com/freebsd/freebsd-src/blob/8d2708de5d6112a492744d367cfdfb8fd219227a/lib/libc/gen/syslog.c): the process-wide mask defaults to 0xff, zero queries it, and nonzero values replace it; masked messages are skipped before formatting, including the unknown-priority diagnostic. The console's mask behaviour is unverified - [fwscanf, vfwscanf, wscanf, vwscanf and their `_s` forms](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) run the `swscanf` scanner on characters read with `fgetwc`, so everything listed for `swscanf` in this file applies to them. As in the console's `libc.prx`, a field takes characters while they can still begin a valid item, so a failed field such as `0x` for `%x` stays consumed, and only the character that ended the scan goes back to the stream. That character is pushed back as its UTF-8 bytes with `ungetc`, because `ungetwc` takes single bytes only; the Windows C runtime guarantees one byte of push-back, so a non-ASCII lookahead can be lost there. A null stream or format returns `EOF`; the console checks them only in the `_s` forms. - [daemon](../../../core/libs/prx/libc/src/Process.cpp) (libc) throws: forking into the background is not supported. PPSA85300 imports it and fails to load without it. VLC 3.0 calls it only when its `daemon` option is set ([libvlc.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/libvlc.c#L190-L197)), and the title starts libVLC with `--ignore-config` and without `--daemon` ([player.cc](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/src/player.cc#L960-L981)) - [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal at end of input returns the number of assignments, including zero, rather than `EOF` as FreeBSD does, as reported from the console scanner in #8479; `%%` at end of input still returns `EOF`. A literal mismatch is a matching failure, where FreeBSD's `vfwscanf` reports an input failure. An incomplete floating exponent such as `1e+` converts its mantissa, as reported from the console scanner in #8479; other incomplete exponent forms are not separately verified. A short `%c` field is currently a matching failure without storing its available prefix, while #9321 reports that the console stores a short `%3lc` field; its return count, partial-output extent and precedence are unverified. An integer field that is only a prefix, such as `0x`, does not convert; FreeBSD's `vfwscanf` pushes back the `x` and converts the `0`. Integers out of range are truncated from `strtoll`/`strtoull`. `%p` follows FreeBSD 11's [vfwscanf.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdio/vfwscanf.c#L610-L615): a hexadecimal integer with an optional `0x`, converted as unsigned and stored as a pointer; a length modifier on it throws, where FreeBSD ignores it. `%S` and `%C`, with any length modifier or `*`, end the scan as a matching failure, as the console's Dinkumware scanner in PPSA21564's `libc.prx` treats them as invalid conversions; this was read from the module, not run on a console, and whether input ending before them is an input failure instead is unverified. It throws for unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title +- [floating scan errno](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) leaves narrow `sscanf` inheriting host `ERANGE` for out-of-range `%f`, `%e` and `%lf`; wide `%Lf` retains host `strtold` errno, so glibc's `ERANGE` for the representable `1e-4950` differs from the bundled library. Wide float and double conversions restore the previous `errno` after `strtof`/`strtod`, as observed in the bundled `libc.prx`; converted values and long-double range handling are retained. - [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The lock is a host [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) lock on one byte at offset 2^63 - 2, which no file reaches, so it excludes other `flock` calls and leaves reads, writes and mapped views alone, like FreeBSD's advisory locks. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. - [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) follows Dinkumware `thread0.cpp` ([VC12](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/thread0.cpp), [STL](https://github.com/microsoft/STL/blob/da52dc0f/stl/src/thread0.cpp)), without console verification. Codes outside 0..6 throw `std::out_of_range`, the deleting destructor throws, and STREAMS errnos absent from FreeBSD 11.4 [`errno.h`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h) (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are omitted. Codes 0..6 throw a 32-byte [VC12 `system_error`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/system_error) with FreeBSD errnos (16, 22, 3, 12, 1, 11, 35), a static `generic_category` (not `_Generic_object`), and unverified 40-byte [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) messages from [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/syserror.cpp) -- [Call tracing](../../../core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) is missing on macOS and records only the entry of a call, not its arguments or result. One process-wide ring keeps the last 65536 calls, so a busy thread pushes the others out; the reporting thread's own calls are listed only while they are still in that ring. Calls are counted per traced target; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; wider vector arguments survive only while the recording code avoids AVX, which is untested. Linux has no crash report, so it prints the trace only on `SIGABRT` (guest or host `abort()`), not on `SIGSEGV`; a caller in a module loaded after the last traced import prints without a module name, and a thread aborting while another writes the report waits for it at most 2 seconds +- [Call tracing](../../../core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) records only the entry of a call, not its arguments or result. One process-wide ring keeps the last 65536 calls, so a busy thread pushes the others out; the reporting thread's own calls are listed only while they are still in that ring. Calls are counted per traced target; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; wider vector arguments survive only while the recording code avoids AVX, which is untested. Linux and macOS have no crash report, so they print the trace only on `SIGABRT` (guest or host `abort()`), not on `SIGSEGV`; a caller in a module loaded after the last traced import prints without a module name, and a thread aborting while another writes the report waits for it at most 2 seconds. An arm64 libc build omits the recorder - [_Dtest](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows Dinkumware double classification matching _FDtest: finite values return -1, subnormals -2, zeros 0, infinities 1, and NaNs 2. +- [_Exp and _FExp](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libc) follow PRAGMATA PPSA02530's bundled `libc.prx` (static disassembly): both operands are classified first and only finite/denormal pairs take the finite path. The strict x-only cutoffs at ±229/±1842 store `0.0*y` without an error below and `y*inf` with overflow above, so an offset does not compensate them. A zero multiplier keeps its sign, a NaN multiplier leaves `*px` unchanged, `+inf` with zero or `-inf` with infinity is invalid, and finite multipliers with `+inf`/`-inf` return signed infinity/zero without flags. Inside the cutoffs subnormals are `Finite` without flags, a zero from scaling raises underflow, and the negative cutoff itself raises nothing. `_FExp` uses the measured `c1`, `c2`, `invln2`, `Q` and `P`; `_Exp` finite constants, exact finite bits, signaling NaNs, directed rounding and flush-to-zero modes remain unverified. +- [exp2f](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`s_exp2f.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_exp2f.c), with `huge` and `twom100` read at run time and `x + redux` rounded to float, as msun's `volatile` reads and `STRICT_ASSIGN` do. As in the `libc.prx` that titles ship, `errno` is set to `ERANGE` only in msun's overflow (`x >= 128`) and underflow (`x <= -150`) branches, so a subnormal result leaves it unchanged. Floating-point exceptions are not compared. It gives the same bits and `errno` as the `exp2f` of the `libc.prx` bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host. - [app0.zar](../../../core/libs/prx/libc/src/AppArchive.cpp) (libc) - a [ZArchive](https://github.com/Exzap/ZArchive) beside `app0/` is a read-only layer under it: a path missing from `app0/` is looked up in the archive, case-insensitively. It is not a console format; titles ship their files in a PFS image instead. Resolving an archive-only path through any call other than the archive-aware libkernel ones throws, so `fopen`, `stat`, `access`, `opendir` and the rest do not read the archive yet, and `opendir` of a directory that exists in both places, `app0/` itself included, and `fdopen` of an archived descriptor throw instead of missing its entries or reading the null device. A damaged compressed block throws instead of returning `EIO` - [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified - [codecvt vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `H-TDszhsYuY`) is thirteen zeroed `uintptr_t` slots matching `st_size` 104 on title `libc.prx` from PPSA26344 and on `libSceLibcInternal.sprx` (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and seven conversion virtuals). Facet calls through the object fault until those methods are wired. - [num_put vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `59oywaaZbJk`) is fourteen zeroed `uintptr_t` slots matching `st_size` 112 on the title `libc.prx` from PPSA16833 (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and eight `do_put` virtuals). Facet calls through the object fault until those methods are wired. +- [time_get vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `o4kt51-uO48`) is thirteen zeroed `uintptr_t` slots matching `st_size` 104 on console `libSceLibcInternal.sprx` (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and seven `do_*` virtuals). Facet calls through the object fault until those methods are wired. +- [time_get vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `sEca1nUOueA`) is thirteen zeroed `uintptr_t` slots matching `st_size` 104 on console `libSceLibcInternal.sprx` (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and seven `do_*` virtuals). Facet calls through the object fault until those methods are wired. +- [num_get vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `Y9C9GeKyZ3A`) is seventeen zeroed `uintptr_t` slots matching `st_size` 136 on console `libSceLibcInternal.sprx` (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and eleven `do_get` virtuals). Facet calls through the object fault until those methods are wired. - [ios_base standard streams](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) `_Addstd` queues the stream in the standard stream table of Dinkumware [ios.cpp](https://github.com/ganboing/crt.vc12/blob/master/src/ios.cpp) (`stdstr`, `stdopens`, `_Addstd`, `_Ios_base_dtor`): it takes the first of slots 1 to 7 that is free or already holds the stream, and slot 8 (`NSTDSTR`) when none is, so slot 8 is shared by every stream past the first seven and registration never fails. It stores the 1-based slot index in the guest `IosBase::standardStream` (Dinkumware's `_Stdstr`, where 0 means not a standard stream) and counts the stream objects registered per slot; registering the same stream again reuses its slot and raises its count. Measured on a PS5 (firmware 13.60) from a homebrew payload calling the system `libSceLibcInternal` export: in a process whose own standard streams already hold slots 1 to 7, twelve distinct streams and a repeated one all get 8, written as a 64-bit value at offset 0x8 with no other byte of the object touched; the assignment of slots 1 to 7 to fresh streams is not observable there and follows VC12. The counts are `int`, not VC12's `char`, and the registration and the destructor lock the table where VC12 locks only `_Addstd`; a slot index past the table throws in the destructor instead of indexing out of bounds. The destructor decrements its slot's count and suppresses the rest of the destruction while the count stays positive, as `_Ios_base_dtor` does, and the slots of destroyed streams keep their pointer and stay reserved for that stream, as `stdstr` does. The streams themselves are destroyed by the title's own code as it registers them through `__cxa_atexit`; when the last reference closes, the unsupported stream storage and callback cleanup is still the throw it was and the locale is released as for a non-standard stream. -- [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. `_Thrd_id` throws because whether the console returns `pthread_t` or a kernel thread ID and at what width is unknown; `_Launch` omits `_Cnd_do_broadcast_at_thread_exit` and calls `pthread_create` registered by libkernel with default attributes. The 32-byte pad layout (`_Go()` vtable at 0x0, `_Cnd_t` at 0x8, `_Mtx_t` at 0x10, `bool _Started` at 0x18) is assumed from VC12, unverified. Failing primitives or thread start throw the guest `std::system_error`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` returns `_Thrd_error` (4) on `pthread_detach` failure and throws when none is registered +- [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), otherwise without console verification. `_Thrd_id` returns `scePthreadSelf()` unchanged, as measured on a PS5 (firmware 13.40) in two threads (#4164, #8711); `_Launch` omits `_Cnd_do_broadcast_at_thread_exit`; the two-argument form starts with default attributes and the three-argument form forwards the caller's attribute pointer, as PPSA02530's `libc.prx` does at `0x18090`. The 32-byte pad layout is assumed from VC12, unverified. Failing primitives or thread start throw the guest `std::system_error`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` returns `_Thrd_error` (4) on `pthread_detach` failure and throws when none is registered - [siginterrupt](../../../core/libs/prx/libc/src/Process.cpp) (libc) runs FreeBSD 11.4's `siginterrupt.c` sequence through the `sigaction` and the `_sigintr` set that libkernel registers at load: it reads the signal's disposition, sets or clears the signal's bit and `SA_RESTART`, and installs the disposition again. The set is written outside libkernel's registration lock, as FreeBSD's libc writes it without one; a call before libkernel has registered throws. - [collate vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `ruZtIwbCFjk`) is nine zeroed `uintptr_t` slots matching `st_size` 72 on the title `libc.prx` from PPSA16833 (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and three `do_*` virtuals). Facet calls through the object fault until those methods are wired; `_Getcat` still installs the host `g_collateVtable` for facets it builds. - [num_get::id, time_get::id and the num_get vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) are data objects, as the public SDK's libSceLibcInternal table declares them and as the other facet ids and the `num_put` and `codecvt` vtables are kept: the ids start at zero and the vtable is 17 zeroed slots, the 136-byte size of `KfcTPbeaOqg` in the console's libSceLibcInternal, whose relocations fill the offset-to-top and typeinfo entries, the two destructors, `locale::facet::_Incref` and `_Decref`, and the eleven `do_get` of Dinkumware's crt.vc12 `xlocnum`, so a guest resolves the symbols and reads data, and a facet call through the vtable faults. libSceAmpr exported the three as throwing functions, which a data consumer reads as code; its qualified imports fall back to libc. - [printf family conversions](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) - Windows `FormatWindows` follows the title `libc.prx` `_Printf` measured from PPSA13934, PPSA07923, PPSA05916 and PPSA04158: `%q`, `%D`, `%O` and `%U` are unknown conversions; an unknown character is printed with its leading `%`, and width, `-` and discarded length modifiers behave as measured (`%5y` is ` %y`, `%hhz` is `%z`). `%%` takes a width. A trailing `%` reads past the format's NUL and is not reproduced; it throws. `#`, precision and a `0` without a width are ignored, as a payload process on a PS5 (firmware 13.60) printed `%#k` and `%.2k` as `%k`; `+`, space, `'` and `0` with a width on an unknown conversion are unverified and throw. On Linux a format with an unknown conversion also goes to this formatter +- [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` +- [freopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) follows C11 K.3.5.2.2 through `freopen`: a null result, mode or stream returns `EINVAL` (22) with `*result` set to null when it exists, and a null name is passed to `freopen`, which reports mode-only reopening as unsupported (`ENOTSUP`, 45) with `*result` null, and the constraint handler is not invoked, as in `fopen_s`. A successful reopen stores the stream in `*result` and returns 0, and a failed one stores null and returns the errno `freopen` left, closing the stream as `freopen` does. The console's return values are unverified diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 9ef8b5e91b..1bfb7d6557 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -6,32 +6,40 @@ - [sceKernelGetSocPowerConsumption](../../../core/libs/prx/libkernel/System/src/ThermalSensors.cpp) (libkernel) - returns `EINVAL` and writes nothing, as `sceKernelGetSocSensorTemperature` does when the host has no sensor: the host's package power is not read (Linux RAPL `energy_uj` is readable by root only, and Windows has no general source). The prototype is not documented. PS5LM Plus (PPSA99581) passes a zeroed 128-byte buffer and, on success, reads its first 32-bit word as milliwatts, accepting 1000 to 350000; on failure it shows 0 W - [scePthreadMutexattrSetprotocol, scePthreadMutexattrGetprotocol, scePthreadMutexattrSetprioceiling, scePthreadMutexattrGetprioceiling](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - `scePthreadMutexattrSetprotocol` accepts 0 to 2 as a PS5 (firmware 13.60) does and returns `EINVAL` otherwise, but the protocol is not applied: priority inheritance and protection are not modelled. On the console a null or uninitialized mutex attribute returns `EINVAL` from the SCE attribute calls, `scePthreadMutexInit` takes an uninitialized attribute as the default one, and a destroyed mutex returns `EINVAL` from `scePthreadMutexDestroy`; a null mutex pointer crashed the payload and still throws - [pthread_mutexattr_setprotocol, pthread_mutexattr_getprotocol, pthread_mutexattr_setprioceiling, pthread_mutexattr_getprioceiling](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - the protocol and the priority ceiling are stored and reported as in [FreeBSD 11 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libthr/thread/thr_mutexattr.c#L192-L245) (the ceiling is set or read only under `PTHREAD_PRIO_PROTECT`, and setting a protocol resets it to the highest `SCHED_RR` priority). A null, uninitialized or destroyed attribute returns `EINVAL` before the other arguments are checked; this error behavior is unverified on the console. Mutexes do not apply the protocol or ceiling: a priority-inherit or priority-protect mutex locks like a normal one and does not change its owner's priority -- [sceCoredumpSetUserDataType](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `Uxqkdta7wEg`) - `(int type)` follows the PS5 12.00 `libkernel.sprx` at 0x138B0: the sign-extended `type` goes to syscall 601 (command 29, third argument 0), and the raw result comes back with no SCE code mapping, so a kernel error is -1 with `errno` set. Which types the kernel accepts is not measured. No dump is produced here, so every type returns 0. Palworld (PPSA23126, #2598) and Hello Neighbor (PPSA35154, discussion #6991) import it; #4144 left it throwing because no signature was known +- [sceCoredumpSetUserDataType](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `Uxqkdta7wEg`) - `(int type)` follows the PS5 13.60 `libkernel.sprx` at 0x138B0: the sign-extended `type` goes to syscall 601 (command 29, third argument 0), and the raw result comes back with no SCE code mapping, so a kernel error is -1 with `errno` set. Which types the kernel accepts is not measured. No dump is produced here, so every type returns 0. Palworld (PPSA23126, #2598) and Hello Neighbor (PPSA35154, discussion #6991) import it; #4144 left it throwing because no signature was known. PPSA02225 also imports it: its one call (at 0x6a1b2) passes 1 and does not read the result. +- [pthread_rwlockattr_settype_np](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) - stores types 1 and 2 through `scePthreadRwlockattrSettype` and returns 0, but the type is not applied: `scePthreadRwlockInit` ignores its attribute. Any other type, 0 included, and a null attribute or object return `EINVAL` and store nothing, as in [BunnyFl's measurements](https://github.com/boykopovar/AnyPS5/pull/9632#issuecomment-6363402814) of firmware 13.42's libkernel. Only the name is public; the `(attr, int)` signature follows the SCE twin and shadPS4, which also registers it as `scePthreadRwlockattrSettype` - [sceKernelGetMainSocId and sceKernelGetCpumode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) both return 0, and [sceKernelGetCpuFrequency](../../../core/libs/prx/libkernel/Time/Time.cpp) returns the host TSC frequency measured for `sceKernelGetTscFrequency`, not the console CPU clock. The PS5's SoC id and CPU mode are unknown; shadPS4's PS4 values are [0x710f10/0x740f30 and 0, 2 or 5 from the param.sfo CPU-mode flags](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1a/src/core/libraries/kernel/process.cpp). PPSA99203 only logs them (`[console] main SoC id: result=%#x out=%#x`, `CPU: %llu Hz, cpumode %d`), passing a pointer that `sceKernelGetMainSocId` leaves untouched - [sceKernelEnableDmemAliasing](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) returns 0 and changes nothing: direct memory can always be mapped more than once here, as `DoMapDirect` in [DirectMemory.cpp](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) only checks that the physical pages are allocated, so there is no aliasing to turn on. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L39-L43) (PS4) sets a flag there that lifts the `EBUSY` its [`sceKernelMapDirectMemory2`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L299-L301), and [`sceKernelMapDirectMemory` for SDK 2.50 and later](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L237-L242), return for direct memory that is [already mapped](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/memory.cpp#L548-L553); that check is not applied here. The no-argument `int` signature follows shadPS4 and ProsperoEden (PPSA99008), which calls it before mapping the same direct memory again. Whether the PS5 refuses such a mapping without the call is unverified - [sceKernelInternalMemoryGetAvailableSize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - internal pool allocation and statistics are not implemented, so a non-null query throws without changing its output. Native null output returns -1; an absent pool returns -2, a failed statistics query returns -3, and success writes total pool bytes minus used bytes. - [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) - the native zero-argument query returns bit 3 of the process sanitizer flags loaded from sysctl `{1, 14, 41}`. The host always reports disabled because guest sanitizer activation is not modeled. +- [pthread_cond_setname_np](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) - takes `(cond, name)` and returns 0 without keeping the name, as `scePthreadCondInit` also ignores its name and nothing reads one back; a statically initialized condition variable is created first, and a destroyed one returns `EINVAL`. FreeBSD 11.4 libthr has no such function; the signature comes only from shadPS4 ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/kernel/threads/condvar.cpp#L80-L88)), which stores the name, returns `EINVAL` for a null `name`, and returns 0 for a statically initialized condition variable without creating it. A null `cond` or a null `name` throws here, because the console's result for either was not measured. ### Unknown function info - [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. +- [scePthreadMutexattrSetkind, scePthreadMutexattrGetkind](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) - `scePthreadMutexattrSetkind` throws `NotImplemented_nid_no_patch` for a kind other than `MutexType::ErrorCheck` (1) to `MutexType::Adaptive` (4): the firmware stores any kind as 16 bits of the type field unchecked, but what a mutex does with such a kind is not measured. Kinds 1 to 4 are set through `scePthreadMutexattrSettype`, and a null attribute or object returns 0x8001FFFF. `scePthreadMutexattrGetkind` takes the attribute itself and returns 0x80020000 plus its kind, or 0x8001FFFF for a null one. The firmware returns 0 for kind 0, which no setter here stores, so that case is untested. Both follow [BunnyFl's measurements](https://github.com/boykopovar/AnyPS5/pull/9632#issuecomment-6363402814) of firmware 13.42's libkernel - [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. - [physhm_open, physhm_unlink](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel, NIDs `n371J5cP+uo` and `AUqJNkobQ1c`) - declared as throwing stubs: the PS5 physical shared memory API is not documented, and [shadPS4's module generator](https://github.com/shadps4-emu/moduleGenerator/blob/main/libraries/kernel.cpp) also declares both with no arguments and returns `ORBIS_OK`, so the signature, the handle a success would return and the console's behaviour are unmeasured. The no-argument `int` signature follows shadPS4. Forever Launcher (PPSA99030) imports both and stops at load on `physhm_open`, and the relinker resolves no call site for either in that title, so no handle is fabricated and both throw - [getlogin_r](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel) returns the host effective account name: `GetUserNameW` as UTF-8 on Windows and `getpwuid_r(geteuid())` on POSIX. A valid account returns a name even without a login session; host `getlogin_r`, `USER` and `LOGNAME` are not consulted. The console's login name and behaviour without a session are unknown. Account lookup or encoding failures, a missing or empty account name, and POSIX account records exceeding the 16 KiB lookup buffer throw; there is no replacement name or retry. A short guest buffer returns `ERANGE` without modifying it, and a null output returns `EFAULT`. +- [getlogin](../../../core/libs/prx/libkernel/System/src/Getlogin.cpp) (libkernel) follows FreeBSD 11 `lib/libc/gen/getlogin.c` and returns a pointer to one static 33-byte buffer filled by `getlogin_r`; on an error it sets `errno` to `getlogin_r`'s code and returns NULL. These semantics are not checked against a PS5. It exports the test seam `LoginNameForTest_nid_no_patch`, which replaces the host account name for tests. +- [setlogin](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel_sys) keeps one name for the process, where FreeBSD 11.4's [`sys_setlogin`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_prot.c#L2138-L2161) sets it for the session, and does not check `PRIV_PROC_SETLOGIN`, which a jailed process fails with `EPERM`; the console's answer is not known. As in FreeBSD, a null name fails with `EFAULT` and a name of 33 bytes or more (`MAXLOGNAME` with the NUL) with `EINVAL`, and nothing is stored. Once a name is stored, `getlogin_r` returns it instead of the host account name described for `getlogin_r`, and ahead of the `LoginNameForTest_nid_no_patch` seam. The name is copied from the guest with the same 33-byte bound as `copyinstr`, so a name with no NUL in its first 33 bytes fails with `EINVAL` without reading past them. - [sceKernelGetSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000); the console's firmware version is not modeled. Measured on PS5 firmware 13.42: record values 0, 24, 39, 40, 41, 48 and 64 in the leading 64-bit field are accepted and preserved, the 28-byte string at offset 8 and packed version at 0x24 are filled, and bytes from 0x28 onward remain untouched. This API reports "13.590.001" / `0x13590001` in that environment, while the Prospero variant reports "13.420.000" / `0x13420000`; their values must not be assumed identical. A null pointer still returns `SCE_KERNEL_ERROR_EFAULT`, which was not measured; other firmware versions and record values remain unverified. - [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_CLK_TCK` returns 128, FreeBSD's `CLK_TCK` as its [`sysconf`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L100-L101) returns it ([`time.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/include/time.h#L48-L54)); the console's value is not checked, and no `times` reports ticks at that rate. CPython's `posix` module stops its import with "cannot read ticks_per_second" when the value is below 1, as the Python in PS5MC (PPSA99420) does. `_SC_ARG_MAX` returns 262144 (FreeBSD's `NCARGS`), `_SC_OPEN_MAX` returns the `RLIMIT_NOFILE` soft limit via `getrlimit` ([`sysconf.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L106-L115)), and `_SC_IOV_MAX` returns 1024 (FreeBSD's `UIO_MAXIOV`). Names other than `_SC_ARG_MAX`, `_SC_CLK_TCK`, `_SC_OPEN_MAX`, `_SC_PAGESIZE`, `_SC_IOV_MAX`, `_SC_NPROCESSORS_CONF`, `_SC_NPROCESSORS_ONLN` and `_SC_PHYS_PAGES` fail with `EINVAL` - [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 - [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](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` (1.1, 1.2, 1.4, 1.10, 6.1), `sysctlbyname` and `sysctlnametomib` serve the names `uname` reads as read-only strings: "FreeBSD", "11.4-RELEASE" and "FreeBSD 11.4-RELEASE\n", the values FreeBSD 11.4-RELEASE builds without source control information ([newvers.sh](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/conf/newvers.sh#L45-L52), [L293-L294](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/conf/newvers.sh#L293-L294)), the host name `gethostname` reports ("AnyPS5") and "amd64"; a short buffer returns `ENOMEM` and a write `EPERM`. The console's strings are not measured -- [kern.osreldate](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - reports MIB `{CTL_KERN, KERN_OSRELDATE}` as `1104001`, FreeBSD 11.4's `__FreeBSD_version` in `sys/sys/param.h`; this is consistent with `kern.osrelease` and `kern.version` above, but the value reported by a PS5 title has not been measured. +- [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` - [kern.boottime](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_KERN`, `KERN_BOOTTIME`} and `sysctlbyname` report a read-only 16-byte `struct timeval`, as FreeBSD 11.4's [`sysctl_kern_boottime`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_tc.c#L144-L163) does: the guest's `CLOCK_REALTIME` minus its `CLOCK_UPTIME`, taken at the first read and kept, so a later step of the host clock does not move it as FreeBSD's `tc_setclock` would. The value a PS5 title gets has not been measured. - [sceKernelGetProsperoSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports system software version `0x01000000` ("01.000.000"); the console's firmware version is not modeled. The 0x28-byte block (a 64-bit size at 0, the version string at 8 and the packed version at 0x24) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which Prospero Multi Tools (PPSA99110) and Prospero Explorer (PPSA99109) are built with and which sets the size to 40, and the same fields in [etaHEN](https://github.com/etaHEN/etaHEN/blob/main/Source%20Code/daemon/include/ipc.hpp) and [shsrv](https://github.com/ps5-payload-dev/shsrv/blob/master/sh.c). A size other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL` without writing; whether the console checks the size is unverified, since etaHEN and shsrv declare a 0x30-byte block and do not set it - [sceKernelGetAllowedSdkVersionOnSystem](../../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports the system software version with the 12 patch bits set, `0x01000fff`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/98fd0689/src/core/libraries/kernel/kernel.cpp) does on the PS4 and as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs) describes it, which Prospero Explorer (PPSA99109) reads it through; a null pointer returns `SCE_KERNEL_ERROR_EINVAL`. The console's value and its errors are unverified -- [sceKernelConvertUtcToLocaltime, sceKernelConvertLocaltimeToUtc](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) convert with the host time zone at the given instant, as libc `localtime` does, instead of the console's time zone setting. `west_sec` holds the standard-time offset in seconds east of UTC, despite its name, and `dst_sec` the daylight saving seconds in effect. This follows PPSA99203 ([PS5SX2](https://github.com/Swordpdf/PS5SX2/blob/b4cf55e87c34e8a2a1cd9302df4373e5a94d28f4/ps5/frontend/fe_ps5.cpp#L54-L63)), whose author saw a console write 7200 and 3600 into the 16-byte block; which zone that console was set to is not recorded, so whether `west_sec` includes daylight saving is not settled. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/23c41333/src/core/libraries/kernel/time.cpp) also stores seconds east. PPSA99203 declares the last argument of `sceKernelConvertUtcToLocaltime` as `uint64_t*`; 32 bits are written. On Windows the standard offset and the daylight saving amount are the C runtime's current `_get_timezone` and `_get_dstbias` for every instant, as libc `localtime_s` uses them, so instants under a zone's earlier rules get today's offsets; a `TZ` variable does not set `_dstbias`, which keeps the value an earlier `_tzset` read from the system (0 when the system zone has no daylight saving), and summer time then adds nothing. In a repeated or skipped local hour, `sceKernelConvertLocaltimeToUtc` takes the offset in effect one standard offset earlier. Its last argument receives the zone's daylight saving amount whether or not it is in effect, found by sampling the host zone over the year around the result, while `dst_sec` is the amount in effect: the libc `mktime` that PPSA99109 bundles (text 0x70ebb-0x70ee5) subtracts that amount when `tm_isdst` is set outside summer time. That `mktime` passes `tm_isdst` as the second argument, which is ignored, so it does not choose the offset in a repeated hour. `sceKernelGettimezone` and the `gettimeofday` time zone still report no offset and no daylight saving. A host time the C runtime cannot convert throws +- [sceKernelConvertUtcToLocaltime, sceKernelConvertLocaltimeToUtc](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) convert with the host time zone at the given instant, as libc `localtime` does, instead of the console's time zone setting. `west_sec` holds the standard-time offset in seconds east of UTC, despite its name, and `dst_sec` the daylight saving seconds in effect. This follows PPSA99203 ([PS5SX2](https://github.com/Swordpdf/PS5SX2/blob/b4cf55e87c34e8a2a1cd9302df4373e5a94d28f4/ps5/frontend/fe_ps5.cpp#L54-L63)), whose author saw a console write 7200 and 3600 into the 16-byte block; which zone that console was set to is not recorded, so whether `west_sec` includes daylight saving is not settled. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/23c41333/src/core/libraries/kernel/time.cpp) also stores seconds east. PPSA99203 declares the last argument of `sceKernelConvertUtcToLocaltime` as `uint64_t*`; 32 bits are written. On Windows the standard offset and the daylight saving amount are the C runtime's current `_get_timezone` and `_get_dstbias` for every instant, as libc `localtime_s` uses them, so instants under a zone's earlier rules get today's offsets; a `TZ` variable does not set `_dstbias`, which keeps the value an earlier `_tzset` read from the system (0 when the system zone has no daylight saving), and summer time then adds nothing. In a repeated or skipped local hour, `sceKernelConvertLocaltimeToUtc` takes the offset in effect one standard offset earlier. Its last argument receives the zone's daylight saving amount whether or not it is in effect, found by sampling the host zone over the year around the result, while `dst_sec` is the amount in effect: the libc `mktime` that PPSA99109 bundles (text 0x70ebb-0x70ee5) subtracts that amount when `tm_isdst` is set outside summer time. That `mktime` passes `tm_isdst` as the second argument, which is ignored, so it does not choose the offset in a repeated hour. The `gettimeofday` time zone still reports no offset and no daylight saving. A host time the C runtime cannot convert throws +- [sceKernelConvertUtcToLocaltime and sceKernelConvertLocaltimeToUtc, measured](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) - a payload process on a PS5 (firmware 13.60) set to UTC-3 with Brazil's history differs from AnyPS5 in two ways. The 16-byte block's first 8 bytes hold the zone's last transition at or before the instant (`1550368800`, the end of summer time in February 2019, for every 2026 and 2038 date; `-57967200` for 1970), not the instant. The last argument is 3600 in every case but `sceKernelConvertLocaltimeToUtc` at 1970-01-01 (0), even in 2026, when the zone has had no summer time since 2019. The offsets, `dst_sec` and the converted times match the zone's history, and both calls return 0 with null outputs. Reproducing the transition needs the zone's full history, which the Windows C runtime does not have +- [sceKernelGettimezone](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) reports the host zone, not the console's time zone setting: `tz_minuteswest` holds the standard offset in minutes east of UTC, despite its name, and `tz_dsttime` the zone's daylight saving amount in minutes, found as for the last argument of `sceKernelConvertLocaltimeToUtc`. A PS5 on firmware 13.40 set to US Eastern returned -300 and 60 (measured 2026-10-19, in summer time); what it returns outside summer time or for a zone without daylight saving was not measured. The Windows limits of the entry above apply. - [getuid, geteuid, getgid, getegid, getresuid, getresgid and issetugid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) report the root identity (0) and a process that never changed credentials (`issetugid` 0), as a jailbroken process has; the host user and group are not exposed to the guest. `getresuid` and `getresgid` follow FreeBSD 11.4's `sys_getresuid` and `sys_getresgid`, writing 0 to non-null pointers and leaving errno untouched on success. The values a title sees on a console are unverified +- [pthread_condattr_getclock](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) - null attribute pointers, null handles and a null output pointer throw; their console error or fault behavior is unverified. The signature follows the public PS5 payload SDK's [pthread.h](https://github.com/ps5-payload-dev/sdk/blob/f5d40f02770e9b0001dd9e18056eca70b3da8b40/include/freebsd/pthread.h). Valid initialized attributes report the stored clock ID, as in [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_condattr.c); this has not been measured on a PS5. Inaccessible non-null pointers and stale handles are not validated. - [sceKernelGetAppInfo](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - only `app_id` and the title ID (offset 0x10, up to 9 characters plus a terminator) are written; callers declare the structure with different sizes, so the rest is left alone. `app_id` is 1, the running title's id in libSceSystemService, which is a guess, not something measured. The error codes follow shadPS4: `EPERM` for another process, and 0 without writing anything for a null pointer. - [AMPR object layout and compact gathers](../../../core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp) - the 24-byte object and offsets (type 0, cursor 4, count 8, capacity 12, pointer 16) follow [ampr_emu assertions](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_emu_command_buffer_types.h#L138-L143) and its [C declaration](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr.h#L41-L47). AnyPS5 represents the 32-bit type word as low 16-bit type and high 16-bit recording flags. Command encoding is AnyPS5 internal: opcode 29 and three 32-bit words encode the high offset byte, length minus one and low offset word. The 12-byte size reduces HFW's 1 MiB buffer pressure; it is not a claim about console opcode encoding. Existing 40-bit offsets and lengths 1 through 2^32 are retained; console behavior outside those limits is unverified. - [APR resolve ForEach result](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - `sceKernelAprResolveFilepathsToIdsAndFileSizesForEach` returns the number of paths that resolved, with the per-path status in `results` and the failed ids/sizes set to the invalid id and 0. The [ampr_emu](https://github.com/drakmor/ampr_emu) bridge passes the count of its resolver through and does not show what is counted; counting successes is not confirmed on a title. The guest errno of the failing non-`ForEach` resolves is set through `__error_nid_postfix` @@ -65,38 +73,41 @@ - [sceKernelSyncOnAddressWait](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded -- [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console +- [scePthreadMutexTimedlock](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp), [scePthreadRwlockTimedrdlock, scePthreadRwlockTimedwrlock](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - a timed lock of a mutex the caller holds, or of a rwlock it holds for writing, sleeps until its timeout before returning `ETIMEDOUT` (`0x8002003c`), as FreeBSD 11.4 libthr does ([mutex](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L878-L928)); whether the console waits is not measured. The prosper oracle measured only the result, for a default mutex and a rwlock with a 1000 us timeout ([kernelx.golden.tsv](https://github.com/mattias800/prosper/blob/3ea5cb9a846b6dcd00f5073360462bbda4f72288/prosper/tests/data/console_oracle/kernelx.golden.tsv#L16-L47)); the other mutex types and the POSIX timed locks follow the same rule unmeasured. +- [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - the mutex's owner is checked before waiting, as in FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029). Measured in a payload process on a PS5 (firmware 13.60): `EPERM` (`0x80020001`) for an unowned mutex of every type, the adaptive initializer (slot 1), an uninitialized slot and a mutex owned by another thread, `EINVAL` for the destroyed mutex (slot 2), and a recursive mutex is released completely and relocked to its depth. `scePthreadCondDestroy` stores 1, which every condition call refuses with `EINVAL`, and leaves an uninitialized slot alone. Cancellation restores the mutex and depth before cleanup and a canceled waiter forwards a wake, per POSIX; its ordering is not measured - [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - opens only flags this process created with attribute 0x100. On the console, other processes' 0x100 flags, such as the shell's `SceShellCoreUtilAppFocus`, can be opened too. They do not exist here, so opening them returns `SCE_KERNEL_ERROR_ESRCH`. A null output with the name of an openable flag returns `SCE_KERNEL_ERROR_EFAULT`, the host's answer to the console's fault there, not a measured code. With a plain flag's name it returns `SCE_KERNEL_ERROR_ESRCH`, as measured; with a missing name it also returns `SCE_KERNEL_ERROR_ESRCH`, which was not measured. - [sceKernelCreateEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - when a call has both an invalid attribute and a name of 32 bytes or more, `SCE_KERNEL_ERROR_EINVAL` is returned, because the attribute checks run before the name length check. The console was only measured with long names and valid attributes (0x20 and 0x120), so which error it returns first is not known. Event flag handles are host addresses here, while the console returns small IDs. - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - one id per request for the Multiple submits and wait mode 2 returning once one request completed follow shadPS4. Requests run synchronously on submit, so cancel never finds one processing. The console's ids are not the small numbers used here (it returned 33, 0x2021, 0x4021) - [sceKernelWaitSema, sceKernelPollSema, sceKernelSignalSema, sceKernelCancelSema, sceKernelDeleteSema](../../../core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp) (libkernel) - `SCE_KERNEL_ERROR_ESRCH` for a null handle follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/60c42b925c3d1e00008621b8587819999260ec31/src/core/libraries/kernel/threads/semaphore.cpp#L329-L366) (PS4), unverified on PS5. Handles are pointers, so a non-null handle that is not a live semaphore is not detected; shadPS4 returns `ESRCH` for any unallocated handle. -- [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; 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 null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked -- [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the returned scheduling policy is unmeasured on a console. Both report 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) +- [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](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) is unmeasured on the console. NID `aO+WS2xWuo4` is named in the firmware 3.20 [export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libkernel.c). Its return and errno contract follows FreeBSD 11 [thr_getprio.c](https://github.com/freebsd/freebsd-src/blob/671fb7df97631020c42ce55527dc49fffe0656b7/lib/libthr/thread/thr_getprio.c): return the recorded guest priority, or -1 with `ESRCH` for an unknown or finished thread. The live-thread registry lock protects the read from a concurrent join. This reports guest scheduler state, not the host scheduling priority. - [pthread_rwlock_reltimedrdlock_np, pthread_rwlock_reltimedwrlock_np](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) take `(rwlock, usec)`, a relative timeout in microseconds, and are `scePthreadRwlockTimedrdlock` and `scePthreadRwlockTimedwrlock` with POSIX error numbers, so they return what those return (`ETIMEDOUT` when the time runs out, at once for 0 while another thread holds the lock, and `EINVAL` for a destroyed lock); a zero lock is initialized as by `pthread_rwlock_rdlock`. FreeBSD 11.4 libthr has no such functions; the signature comes only from shadPS4, which registers one function for each NID and its `scePthreadRwlockTimed*` twin ([`rwlock.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/rwlock.cpp#L167-L179), 32-bit `usec`). Unverified on the console - [sceKernelGetAvailableCpumask](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - returns the default thread affinity, 0x1FFF (CPUs 0 to 12), as KytyPS5 does. Titles pin threads within it (PPSA02664 passes 0x1FFB, Hades II PPSA36082 0x3 and 0x3F); whether CPU 13 is available to a title is not confirmed - [sceKernelGetCurrentCpu](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - guest affinities are not applied to host threads, so the host CPU index is mapped into the calling thread's affinity clipped to `sceKernelGetAvailableCpumask` (the available mask when they do not overlap): host CPU n reports the (n modulo the CPU count)-th CPU of that mask, so a thread pinned to one CPU reports that CPU, as a FreeBSD `cpuset` guarantees on the console. Which CPU the console reports for a thread allowed on several is not known; shadPS4 returns 0 - [pthread_attr_setsolosched_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) takes `(attr, int)` and stores the value through `scePthreadAttrSetsolosched`, returning `EINVAL` (22) for a null or uninitialized attribute like the other POSIX attribute setters. Only the name is public (aerolib `2+pVfgiEd7A`); the signature and behaviour follow the SCE twin, which [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/pthread.cpp#L2188-L2199) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs#L891-L913) store as an `int` without a range check, and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/71de7681f06c5db59a1120a3bb554c86780c3f5f/src/hle/libs/kernel_threading.zig#L1396-L1398) implements this alias over it. On a PS5 (firmware 13.60) only 0 and 16 are accepted; every other value returns `ENOTSUP` (45, `0x8002002D` from the SCE call) and keeps the stored one, and the SCE calls return `EINVAL` for a null or uninitialized attribute. PPSA15878 passes 16. Threads created from the attribute are not scheduled differently; what solo scheduling does on the console is unknown. Returnal (PPSA01285 v03.000.005) stopped on it with `what(): pthread_attr_setsolosched_np_nid_postfix not implemented` (#2872, #1155); LEGO Horizon Adventures (PPSA14632, #734) and Palworld (PPSA23126, #2598) import it -- [sceKernelMapFlexibleMemory / sceKernelMapNamedFlexibleMemoryInternal](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - flag 0x8000 is accepted and ignored; its meaning is not established. KytyPS5 (`GUEST_MAP_UNKNOWN_8000`) and sharpemu accept it without reading it. Every known caller is an SDK 9 `libc.prx` heap, which passes it with read/write protection and uses the mapping as an older `libc.prx` uses `sceKernelMapNamedFlexibleMemory` with flags 0: through the Internal variant in PPSA21564, through `sceKernelMapFlexibleMemory` in PPSA22520 01.000.011 +- [pthread_main_np and scePthreadMain](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel_sys) - console behaviour is unmeasured. Linux identifies the process initial thread by kernel TID/PID equality, including worker-first loading. Other hosts identify the thread that initialized libkernel; loading it first from a worker can misidentify the initial thread. The pre-initialization -1 state is not exposed. +- [sceKernelMapFlexibleMemory / sceKernelMapNamedFlexibleMemoryInternal](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - flag 0x8000 is accepted and ignored by both, as KytyPS5 (`GUEST_MAP_UNKNOWN_8000`) and sharpemu do; its meaning is unknown. In a PS5 (firmware 13.60) payload process the Internal variant accepted it with the address, `sceKernelVirtualQuery` record and protection unchanged; `sceKernelMapFlexibleMemory` and `sceKernelMapNamedFlexibleMemory` failed it with `EINVAL`, and the first failed every other single flag from 0x1 to 0x100000 except 0x80 (0x10 untried). Here those flags, and 0x8000 in `sceKernelMapNamedFlexibleMemory`, throw; the other two accept 0x8000 because PPSA22520 passes it to the first. Every known caller is an SDK 9 `libc.prx` heap passing it with read/write protection and using the mapping as older ones use `sceKernelMapNamedFlexibleMemory` with flags 0 (Internal in PPSA21564, `sceKernelMapFlexibleMemory` in PPSA22520) - [sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - sets the memory type of the direct mappings in the page-rounded range and of the direct memory behind them, as shadPS4 and prosper do (KytyPS5 ignores the type); the type is not validated, flexible pages in the range only get the protection, and a later map of the retyped direct memory reports the type it was allocated with. `sceKernelBatchMap2` operation 4 takes the same path with the entry type - [Map hints inside a reservation](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMapDirectMemory` and the flexible and anonymous maps place a mapping without `MAP_FIXED` at its hint address when the whole range lies in a range still reserved by `sceKernelReserveVirtualRange`, as if `MAP_FIXED` were set. Taken from PPSA01474, which reserves 64 GiB and then maps its heaps inside it with hints and no `MAP_FIXED`, and uses the hint addresses afterwards; not measured on the console. Hints outside a reservation, or over a range that is partly mapped, stay hints - [Fixed anonymous mmap](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `MAP_PRIVATE | MAP_ANON | MAP_FIXED` replaces the requested range through the existing guest mapping machinery and zeroes the new memory. `MAP_EXCL` is checked under the allocation-registry mutation lock, including virtual reservations, and returns `ENOMEM` without changing an occupied range; without `MAP_FIXED` it is `EINVAL`. Lengths are rounded to 16 KiB and fixed addresses must be aligned. The complete rounded range must lie inside the guest arena or, for POSIX fixed anonymous `mmap` on Windows, one current or registered native thread stack; other mapping APIs do not opt into stack replacement. Native stack replacement keeps the reservation, decommits and recommits fresh pages, and rejects foreign allocations, ranges spanning reservations and host `PAGE_GUARD` pages before mutation. Current-stack ranges must end at least 64 KiB below the active host frame; callers replacing another thread's stack must coordinate with that thread. If recommitting fails, the call returns `ENOMEM` with the range still reserved; thread cleanup attempts to recommit it before restoring write access. This follows FreeBSD 11.4 [argument validation and rounding](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L226-L299), [fixed replacement](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1455-L1464), and [collision errno](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1604-L1613). The console behaviour has not been measured. Linux and Windows accept addresses from 64 KiB that no host mapping occupies, such as the 0x400000 imported by PICO-8 PPSA99808; macOS keeps the 0x200000000 start. Non-fixed private anonymous mappings keep their hint; null hints still use the 8 GiB area. On Windows the host's `KUSER_SHARED_DATA` always occupies 0x7ffe0000, so Wine's shared user data page (Forever Launcher PPSA99030) cannot be placed there. The console address-space layout is unmeasured; executable placement outside the host arena is tracked in #5217. Static catalog disassembly finds `0x1012` in PPSA98001/PPSA99808 and `0x5012` in PPSA99808; these titles were not run. - [Mapping hints with no room above them](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - a mapping without `MAP_FIXED` whose hint lies above the guest address space, or has no free range at or above it, is placed from the bottom of the guest arena, as FreeBSD 11.4's [`vm_mmap_object`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1569) retries through [`vm_map_find_min`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1555-L1584) from its minimum address. FreeBSD's minimum is the end of the largest possible data segment rather than the start of the arena, and the console's address-space layout is not measured. PS5CEMU-HAR (PPSA99360) gives `sceKernelReserveVirtualRange` a hint near its own code, which is a host address here because the relinked executable lies outside the guest arena, so its JIT gets memory more than 2 GiB from its code and does not start -- [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in the [fpPS4](https://github.com/red-prig/fpPS4) reimplementation, Increment takes a reference on the loaded image containing the address and Decrement releases one; the variants without the leading underscore are assumed to match +- [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in a payload process on a PS5 (firmware 13.60), Increment returns -1 for an address outside every module (null, stack, heap) and 0 otherwise, and Decrement never fails 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 - [cfsetspeed](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel, NID `C1MQ6OaXRjo`) - console behavior and module/library versions are unverified. The prototype, uint32 speed and 44-byte termios layout (input speed at 36, output speed at 40) follow the public PS5 payload SDK [termios.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/termios.h) and [sys/_termios.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/sys/_termios.h). The module association follows its libkernel stub table and the NID rehashes to the aerolib name. Both speed fields accept every uint32 value and no flags or control characters change, as [FreeBSD 11.4 termios.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/gen/termios.c) does. No terminal I/O is performed. Null pointers throw where the reference dereferences them. -- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` follow `randomdev_poll` for a seeded device under [`selscan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1178-L1236), which asks once with the flags of every set the descriptor is in: in the read set they are readable and reported in no other set, and otherwise they are reported in the write and exception sets they were asked in. A regular file, a directory or a file read from app0.zar is always readable and writable and never in the exception set, as `vop_stdpoll` answers, on every host. Guest sockets and libSceNet sockets are polled. A kqueue is readable while one of its events is triggered and is never writable or in the exception set, as FreeBSD 11.4's [`kqueue_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c#L1900-L1924) answers, on every host. Other descriptors are polled through the host on Linux, while on Windows pipes are checked every millisecond with the readiness test `poll` uses and any other descriptor throws. Dungeons (PPSA16064) selects on descriptor 0, and RetroArch (PPSA99169) selects on its web server's wake-up pipe next to its listening socket -- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown that is below `GuestSockets::FirstDescriptor` is polled on the host instead of throwing (on Windows only a pipe, as above); an invalid host descriptor fails with `EBADF`, and mixed host and socket sets are polled in short alternating slices so neither side waits out the full slice alone. A descriptor that stops being a socket, a kqueue or a pipe during the wait fails the call with `EBADF` when it is closed and is selected again with the rest of the timeout when its number was reused, as FreeBSD 11.4's [`kern_select`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1129-L1140) looks every descriptor up again in each pass. A close that has already removed the descriptor from the guest's kqueue, pipe or file tables but not yet closed its host descriptor is seen by that new look-up as a descriptor of unknown kind, which throws on Windows and is polled through the host on Linux. Wobbly Life (PPSA29413) selects on file descriptors while streaming its bundles +- [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 - [__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 - [pthread_getname_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - a thread that has already been joined returns `ESRCH` (3) and its output buffer is left untouched, as measured on a PS5 (firmware 13.60) by the author of this entry; since [#4481](https://github.com/boykopovar/AnyPS5/pull/4481) the joined thread's object is retained, so the posix wrapper answers `ESRCH` for it instead of reading the retained name. A null name buffer is answered with `EFAULT` before that, an order the console table does not measure. The SCE sibling `scePthreadGetname` has no measured joined-thread behaviour on the console and still reads the retained object's name, so it is unchanged, as is the pre-existing dangling case of a thread that ended detached (its object is released at exit; see the scePthreadJoin entry) - [sceKernelGetOperationMode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call and ignores the result. A homebrew payload on a PS5 (firmware 13.60) reads mode 2 and submode 0, nothing else is written, and the result register holds no status, so the function is taken to return nothing. Whether a title process reads the same values is unverified. Null outputs throw -- [sceCoredumpGetStopInfoCpu](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) has no active coredump context. Native null/size validation is recovered, including both 8-byte and 16-byte records; valid requests return `NOT_IN_COREDUMP_HANDLER` without accessing the output. Active-handler data retrieval remains unimplemented. +- [sceCoredumpGetStopInfoCpu](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) has no active coredump context. Native null/size validation is recovered, including both 8-byte and 16-byte records; valid requests return `NOT_IN_COREDUMP_HANDLER` without accessing the output. Active-handler data retrieval remains unimplemented. One title (PPSA02225) disassembled for this entry calls it once (arguments set up at 0x8d67dd and 0x8d67e1) with a stack buffer and the length 0x10: for 0x81180005 it branches away, for 0 it reads a 32-bit code at offset 8 of the buffer, and for any other nonzero result, which includes the `NOT_IN_COREDUMP_HANDLER` result returned here, it formats an error text containing the code. The call does not show what the console returns outside a coredump handler. - [sceCoredumpGetThreadContextInfo](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) validates a nonnull 144-byte output using the native checks. Active-handler register capture remains unimplemented; valid requests return `NOT_IN_COREDUMP_HANDLER` without accessing the output. - [sceCoredumpAttachMemoryRegion, sceCoredumpAttachUserMemoryFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) cannot attach memory to an active coredump. The native signed 32-bit length is recovered: negative lengths are ignored successfully; other calls return the outside-handler error without accessing memory. Capture and kernel-side record limits remain unimplemented. -- [sceCoredumpWriteUserData, sceCoredumpDebugTextOut](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) have no active coredump report. WriteUserData validates a nonnull buffer and size at most `INT32_MAX`, including zero; accepted requests return the sign-extended outside-handler error. DebugTextOut still prints to stderr instead of a report; its validation and report integration remain unverified. -- [sceCoredumpAttachUserFile, sceCoredumpAttachMemoryRegionAsUserFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) cannot write files to an active coredump. Native null-name checks, the signed 32-bit memory length and SDK 6.50 flag validation are recovered. Nonnegative requests return the outside-handler error; negative memory lengths are ignored after name/flag validation. Kernel-side path handling and capture limits remain unimplemented. +- [sceCoredumpWriteUserData, sceCoredumpDebugTextOut](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) have no active coredump report. WriteUserData validates a nonnull buffer and size at most `INT32_MAX`, including zero; accepted requests return the sign-extended outside-handler error. DebugTextOut still prints to stderr instead of a report; its validation and report integration remain unverified. One title (PPSA02225) disassembled for this entry makes six calls to `sceCoredumpWriteUserData` (at 0x6a1c4, 0x6a1e1, 0x6a1fe, 0x6a21b, 0x6a235 and 0x6a24e) right after `sceCoredumpSetUserDataType(1)`: five with a length of 1 and one with a length of 5. After each call it stores a negative result in a 32-bit field of its own object and ignores other results, so the outside-handler error returned here would be stored. What the bytes mean is not shown. +- [sceCoredumpAttachUserFile, sceCoredumpAttachMemoryRegionAsUserFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) cannot write files to an active coredump. Native null-name checks, the signed 32-bit memory length and SDK 6.50 flag validation are recovered. Nonnegative requests return the outside-handler error; negative memory lengths are ignored after name/flag validation. Kernel-side path handling and capture limits remain unimplemented. One title (PPSA21480) disassembled for this entry calls `sceCoredumpAttachMemoryRegionAsUserFile` (0x83177-0x83186) with 0, a stack buffer passed to a helper first, the length 0x1618 and a file name string, only when a flag of the title is set, and `sceCoredumpAttachUserFile` (0x831cb, 0x831d2) with 0 and a string, only when a method of the title returns a nonzero pointer. It checks both results only for a negative value. What the first argument 0 and the strings mean on the console is not shown. - [sceKernelAddTimerEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - `usec` is taken as the period in microseconds; re-adding an existing timer keeps its pending expiration count and restarts the period from the time of the call, where FreeBSD 12+ `filt_timertouch` clears pending expirations on `EV_ADD`; `usec == 0` fires on every wait. None of this is confirmed on a PS5 title - [sceKernelWaitEqueue](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - a level-triggered user event (`sceKernelAddUserEvent`) is reported at most once per call, as in FreeBSD `kqueue_scan` (`sys/kern/kern_event.c`), which re-queues a reported knote without `EV_CLEAR` or `EV_DISPATCH` behind the scan marker, and kevent(2), where several triggers do not produce several kevents. Not measured on a console. A reported event moves to the end of the list, so events are reported in the order they were last reported, and those never reported come first in registration order. FreeBSD `knote_enqueue` queues them in the order they fire. Several completions of one AMPR or VideoOut event within one call are returned as separate entries, where FreeBSD would aggregate them; the Orbis behaviour is unconfirmed - [AC1FtjqMCL0](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature @@ -116,9 +127,8 @@ - [pnz21I6VjDs](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature - [qk043QPsrZU](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature - [uDGXwUKqi8s](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature -- [sceKernelGetCpuTemperature, sceKernelGetSocSensorTemperature](../../../core/libs/prx/libkernel/System/src/ThermalSensors.cpp) (libkernel) - read the host's thermal sensors: on Linux from `/sys/class/hwmon` and `/sys/class/thermal`, on Windows from the ACPI thermal zones (`MSAcpi_ThermalZoneTemperature` over WMI) and the storage temperature property of the first physical drive. Sensor 0 is the CPU/package value, 1 the first other value and 2 an NVMe value; a host that exposes none reports every sensor as unsupported (`EINVAL`). The console's SoC channel count and meanings are hardware-dependent and differ from the host's, the host sensors are not the console's, the ACPI reading is unavailable on many machines, and the storage temperature property is numbered 52 on Windows 10 2004 and later and 22 before it, so the mapping, the values and the unsupported-input error are unverified; the storage channel was checked against the drive's own SMART composite temperature on one NVMe host (`StorageDeviceTemperatureProperty`, 36 °C on a WD_BLACK SN7100). `sceKernelGetCpuTemperature` reports sensor 0. PPSA99058 and PPSA99420 call both -- [LinuxProtFromSce](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - protection bits 0x100 and 0x200 are accepted and assumed not to affect host CPU access; host protection is derived only from the read, write and execute bits -- [getdtablesize](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - returns the smaller of the `RLIMIT_NOFILE` soft limit of `getrlimit` and the host's `getdtablesize()`, capped at `INT_MAX`: FreeBSD returns the smaller of the `RLIMIT_NOFILE` soft limit and `maxfilesperproc` ([`sys_getdtablesize`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L338-L355)), and the host call supplies that bound on macOS, where the soft limit alone is not one (`setrlimit` accepts soft limits above `maxfilesperproc`, `RLIM_INFINITY` included); `maxfilesperproc` is not measured on the console. The guest limit is the host's soft limit, raised to the hard limit when libkernel loads only on Linux: `core/libs/prx/libkernel/File/src/OpenFileLimit.cpp` is `__linux__`-only, so on macOS the guest gets whatever soft limit the host process inherited. VLC (PPSA85300) imports it for a `closefrom` loop in the child of its spawn helper ([libc_stubs.c](https://github.com/ZiZc3/VLC/blob/d46be6a68f4244466b041c2dc1b640915ac1a983/ps5/compat/libc_stubs.c#L66-L72)), which never runs because its `fork` fails with `ENOSYS` +- [sceKernelGetCpuTemperature, sceKernelGetSocSensorTemperature](../../../core/libs/prx/libkernel/System/src/ThermalSensors.cpp) (libkernel) - channels 3 to 64 throw, and a channel the host has no sensor for returns `EINVAL`, where a payload process on PS5 firmware 13.60 read 49 to 54 °C from channels 0 to 63 and always -49 from 64; a null output with a valid channel returns `EFAULT`, where the console faults. Otherwise they follow the console: a channel outside 0 to 64 returns `EINVAL` before the output is looked at, and `errno` is set with the error. The values come from the host: on Linux `/sys/class/hwmon` and `/sys/class/thermal`, on Windows the ACPI thermal zones over WMI and the storage temperature property of the first physical drive (52 on Windows 10 2004 and later, 22 before). Channel 0 is the CPU/package value, 1 the first other value and 2 an NVMe value; `sceKernelGetCpuTemperature` reports channel 0. PPSA99058 and PPSA99420 call both +- [getdtablesize](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - returns the smaller of the `RLIMIT_NOFILE` soft limit of `getrlimit` and the host's `getdtablesize()`, capped at `INT_MAX`: FreeBSD returns the smaller of the `RLIMIT_NOFILE` soft limit and `maxfilesperproc` ([`sys_getdtablesize`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L338-L355)), and the host call supplies that bound on macOS, where the soft limit alone is not one (`setrlimit` accepts soft limits above `maxfilesperproc`, `RLIM_INFINITY` included); `maxfilesperproc` is not measured on the console. On Linux and macOS the guest limit is the host's soft limit. VLC (PPSA85300) imports it for a `closefrom` loop in the child of its spawn helper ([libc_stubs.c](https://github.com/ZiZc3/VLC/blob/d46be6a68f4244466b041c2dc1b640915ac1a983/ps5/compat/libc_stubs.c#L66-L72)), which never runs because its `fork` fails with `ENOSYS` - [close, sceKernelClose](../../../core/libs/prx/libkernel/File/src/HostStreams.cpp) (libkernel) - descriptors 0 to 2 that are open when libkernel loads belong to the host process: closing one fails with `EPERM` (`sceKernelClose`: `0x80020001`) and leaves it open, as closing the open descriptors 0 to 2 of a payload process did on a PS5 (firmware 13.60); whether a title process can close them is unverified. PPSA21703 calls `sceKernelClose(0)` repeatedly; closing the host stream let the host C runtime give descriptor 0 to the next file opened, which the next call then closed - [sceKernelPrintBacktraceWithModuleInfo](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws @@ -127,6 +137,8 @@ ### Functional - [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`, which no library defines yet; 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. +- [sceKernelGetTscFrequency](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) pins its thread to the CPU it runs on for the 20 ms measurement, so that both TSC reads come from one counter. If the pin can't be set (`sched_getcpu` or `pthread_getaffinity_np` fails on Linux, `SetThreadAffinityMask` fails on Windows), the measurement runs unpinned. On a host whose CPU TSCs differ, the result can then be wrong as before. Neither call is known to fail for the calling thread's own CPU, which is always in its allowed set. macOS never pins. - [Guest thread scheduling](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - the policies and priorities that `sched_get_priority_max`, `sched_get_priority_min`, `pthread_setschedparam` and `scePthreadSetschedparam` accept, and those a created thread takes, follow a PS5 (firmware 13.60), but they are only recorded and reported: they are not applied to host threads, so a title's priority order has no effect on host scheduling - [Linux runtime module initialization](../../../core/libs/prx/libkernel/Module/src/DynamicLoader.cpp) (libkernel) - modules in explicit runtime directories can be mapped for bindings before an explicit guest load runs their entry and init array once with the caller arguments. Finalizers run only after initialization completed. Console dependency-start and finalization ordering are unmeasured. Windows and macOS use their existing lifecycle paths. @@ -137,7 +149,7 @@ - [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 - [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; 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`. Console behavior for null and invalid timeouts when a count is available is unverified. +- [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. - [kevent measurements](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - in payload processes on a PS5 (firmware 12.70), re-adding an existing periodic `EVFILT_TIMER` with `EV_ADD`, `fflags` 0 and a positive period kept the armed expiry and the expirations already counted and applied the new period after that expiry, as FreeBSD 11.0 does, where 11.4's `filt_timertouch` restarts the timer: a 50 ms timer re-added with 10 ms fired first 46 ms after the re-add and then every 10 ms, a 10 ms timer re-added with 100 ms fired first 5 ms after the re-add and then every 100 ms, the 12 expirations counted before a re-add were reported after it, and `EV_ONESHOT` in the re-add left the timer periodic. Here such a re-add does the same; re-adding a one-shot or disabled timer, a re-add with nonzero `fflags` (`NOTE_MSECONDS` included), a period of 0 or less, a period too large to add to the armed expiry or flags other than `EV_ONESHOT` were not measured and throw here, and the other changes to an existing timer except `EV_DELETE` throw here too. There `EV_ADD` failed with `EPERM` for `EVFILT_AIO`, `EVFILT_LIO` and `EVFILT_SENDFILE` at every ident tried (0, 5, 999, 2^40 and an open pipe) and, at ident 5, also with `EV_CLEAR`, `EV_ONESHOT` or `EV_DISABLE` added, and `EV_DELETE` and `EV_ENABLE` without `EV_ADD` failed with `ENOENT`. `EVFILT_PROCDESC` failed with `EBADF` on a descriptor that is not open and on an ident above `INT_MAX`, and with `EINVAL` on every open descriptor tried: Unix-domain stream and datagram sockets, descriptor 0 (a socket), an `AF_INET` UDP socket, both ends of a pipe, a regular file, a directory, another kqueue and the kqueue itself; `EV_DELETE` on a socket failed with `ENOENT`. Here these four filters answer the same way. `EV_ADD` of `EVFILT_PROCDESC` fails with `EINVAL` on the kinds of descriptor tried, since there are no process descriptors here, and also on shared memory and JIT shared memory, which are host files here: FreeBSD 11.0 gives `shm_open` objects [`invfo_kqfilter`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_shm.c#L136), which answers `EINVAL`, but neither was measured. It throws on any other kind, such as `/dev/random` and `/dev/urandom`, and on descriptors 0 to 2 that are not sockets or kqueues, whatever they hold: the host's standard streams, a pipe or file that `dup2` put on one of them, or a descriptor opened on a number that was free when libkernel loaded. A change of any of the four without `EV_ADD` fails with `ENOENT`, for `EVFILT_PROCDESC` after the descriptor check, as `kqueue_register` finds no knote. When `EV_ADD` of one of the four fails with `EPERM`, or with `EINVAL` on a descriptor other than the kqueue itself, the error event has `fflags` 0 and the errno in `data`, as FreeBSD 11.0's [`kqueue_register`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_event.c#L1302-L1316) clears `fflags` and `data` before the attach and [`kqueue_kevent`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_event.c#L1005-L1006) then stores the error in `data`; a descriptor that is not open, the kqueue itself and `EV_ENABLE` with `EV_DISABLE` fail before that and keep their `fflags`. That error event was not measured. `EVFILT_PROC` on the process itself with `NOTE_EXIT`, `EVFILT_SIGNAL` for `SIGUSR1` and `EVFILT_FS` at ident 0 were accepted there and still throw here. Also measured there on Unix-domain socketpairs, and still throwing here: a `NOTE_LOWAT` mark of 0 on an empty stream socket gave a read event at once (`data` 0), and so did re-adding that knote with a mark of -1 once the socket was drained; a write mark of 2 on the other end of that stream socketpair gave an event (`data` 8192), and re-adding that knote with 2^30 gave none; a mark of 5 on a datagram socket gave no read event after one 1-byte datagram and a read event (`data` 11) after a second, 10-byte datagram. `EVFILT_READ` on another kqueue reported the number of events pending in it there (1, then 2) and throws here; `EVFILT_WRITE` on a kqueue and a kqueue registered on itself failed with `EINVAL` there, as they do here - [tcsendbreak, tcdrain, tcflush, tcflow, tcgetpgrp, tcsetpgrp](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) follow FreeBSD 11.4's [`termios.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/termios.c): `tcflush` answers `EINVAL` for a queue other than `TCIFLUSH`, `TCOFLUSH` or `TCIOFLUSH` and `tcflow` for an action outside `TCOOFF`..`TCION` before the descriptor is looked at, and every other call fails as its terminal ioctl does on a descriptor that is not a terminal, with the errors `tcgetattr` gives, so `tcsendbreak` never reaches its 0.4 s pause. FreeBSD's `pipe_ioctl` answers `TIOCGPGRP` and `TIOCSPGRP` on a pipe or FIFO with the descriptor's SIGIO owner, which is not modelled, so `tcgetpgrp` and `tcsetpgrp` throw on a host pipe or FIFO above descriptor 2; descriptors 0 to 2 are taken as non-terminals that are not pipes, whatever the host attaches to them, so a pipe the guest creates on a freed descriptor 0 to 2 gets `ENOTTY` there. The PS5 `libkernel` is assumed to use this FreeBSD code, and `tcdrain` is not a cancellation point here - [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Directory timestamp updates resolve the current path of the opened handle. Filesystems with coarser timestamp resolution cannot retain every guest microsecond. Birth-time clamping is not atomic with concurrent host timestamp updates and has not been measured on a console @@ -149,10 +161,11 @@ - On Linux, [direct memory](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [system](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [sceKernelWriteThrottlingStatus](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed; returns `EFAULT` (`0x8002000e`) when passed a null status buffer pointer. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown -- [Windows directory descriptors](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. Names whose UTF-8 encoding exceeds the one-byte `d_namlen` limit are not representable in this guest directory-record layout: `getdents` returns `ENAMETOOLONG` without consuming the entry. Repeated calls therefore report the same error and entries after that name cannot be reached through this descriptor; FreeBSD's `getdents` behavior for host filenames that exceed its `MAXNAMLEN` is not verified. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies. Descriptors retain the opened directory's native handle across renames. Enumeration reloads and `*at` operations resolve its current path, so a concurrent rename between resolution and the operation is not atomic. Enumeration of a removed directory and network-share path queries have not been verified +- [Windows directory descriptors](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. Names whose UTF-8 encoding exceeds the one-byte `d_namlen` limit are not representable in this guest directory-record layout, so `getdents` and `getdirentries` throw when they reach one; FreeBSD's behavior for host filenames that exceed its `MAXNAMLEN` is not verified. `getdirentries` reports through `basep` the entry index its block starts at, so only 0 and the values from `basep` or `lseek` are valid offsets; FreeBSD offsets are filesystem cookies. Descriptors retain the opened directory's native handle across renames. Enumeration reloads and `*at` operations resolve its current path, so a concurrent rename between resolution and the operation is not atomic. Enumeration of a removed directory and network-share path queries have not been verified - [Linux directory entry types](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) - `sceKernelGetdirentries` reports a host symlink inside the app0 tree as the type of its target (directory or regular file), since a console app0 has no links and links there come from how the title was unpacked; a link elsewhere, or a dangling one, stays a link. A directory opened through an app0 link resolves to its target's host path, so links nested inside that target keep the link type. -- [ftruncate, truncate, sceKernelFtruncate](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) extend a file on Windows with `SetFileInformationByHandle` (`FileEndOfFileInfo`), which reserves the clusters but writes no zero bytes, and report `EFBIG` for a length the volume cannot hold (`ERROR_INVALID_PARAMETER` on a disk file; seen with a length of `INT64_MAX`, the size from which NTFS refuses it was not measured) and `ENOSPC` when the free space is smaller. FreeBSD 11.4 makes the extension a hole on a file system that has them ([`ffs_truncate`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/ufs/ffs/ffs_inode.c) writes only the last block), so a length above the free space succeeds there. A write behind the end of the file still makes NTFS write zero bytes over the gap, as the file is not sparse. None of this was measured on a console. -- [readv, writev, preadv, pwritev and their sceKernel forms](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) - unreadable iovec-table errors follow FreeBSD 11.4's [`copyinuio`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/subr_uio.c#L401-L434), not console measurements. The table is copied before inspecting its entries; payload-buffer validation is unchanged. A Linux `process_vm_readv` failure other than `EFAULT` throws, including when the host denies that API. +- [ftruncate, truncate, sceKernelFtruncate](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) extend a file on Windows with `SetFileInformationByHandle` (`FileEndOfFileInfo`), which reserves the clusters but writes no zero bytes, and report `EFBIG` for a length the volume cannot hold (`ERROR_INVALID_PARAMETER` on a disk file; seen with a length of `INT64_MAX`, the size from which NTFS refuses it was not measured) and `ENOSPC` when the free space is smaller. FreeBSD 11.4 makes the extension a hole on a file system that has them ([`ffs_truncate`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/ufs/ffs/ffs_inode.c) writes only the last block), so a length above the free space succeeds there. A write behind the end of the file still makes NTFS write zero bytes over the gap, as the file is not sparse. None of this was measured on a console. One title (PPSA04354) calls it at 0xe9d4f2 and tests only the sign of the result; the lengths it passes are not examined. +- [Windows directory entry types](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) - types other than regular files, directories and symbolic links, and reparse points unsupported by `lstat`, throw. If the host cannot stat an entry, its type is `DT_UNKNOWN`. Console directory-entry types have not been measured; the host `lstat` modes map to `DT_REG`, `DT_DIR` and `DT_LNK` following [FreeBSD 11.4 dirent.h](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/sys/dirent.h). A host link inside canonical app0 is reported as its target's type (directory or regular file); a link elsewhere, or a dangling one, stays a link. A directory opened through an app0 link resolves to its target's host path, so links nested inside a target outside app0 keep the link type. +- [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. - [/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 - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing - [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. @@ -160,27 +173,28 @@ - [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) and [setsockopt](../../../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` polls them; 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) and [setsockopt](../../../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 - [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 - [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 - [sceKernelAioSetParam](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - the limits and `sceKernelAioInitializeParam`'s defaults (three 20-byte scheduling structs, the last without split) are measured on a PS5 (firmware 13.60), where a null struct crashes both calls; here both throw. The scheduling settings are not used: `sceKernelAioInitializeImpl` ignores the struct and requests run synchronously on submit - [sceKernelGetFsSandboxRandomWord](../../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist, except `/gkbHtXc3Wq/common/cert/CA_LIST.cer`, which [SandboxCertificates.cpp](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) serves from the host trust store - [ktimer_create, ktimer_settime, ktimer_gettime, ktimer_getoverrun, ktimer_delete](../../../core/libs/prx/libkernel/Time/Ktimer.cpp) (libkernel) - a `SIGEV_SIGNAL` or `SIGEV_THREAD_ID` timer, and one created with a null `sigevent` (FreeBSD defaults it to `SIGEV_SIGNAL`), throws, because delivering a guest signal from a timer is not modelled. A non-zero value under one tick rounds up to 1 ms, as FreeBSD 11 does at hz 1000; the console's hz is not measured (`kern.clockrate` is unanswered). `SIGEV_NONE` timers on `CLOCK_REALTIME` and `CLOCK_MONOTONIC` follow `kern_time.c`: ids 3 to 31 (`EAGAIN` when full), `EINVAL` for another clock, a bad notify type or signal, an id that is not live or a bad `itimerspec` field, and `TIMER_ABSTIME` (1). Expiry is worked out from the guest clock when the timer is read, and `ktimer_getoverrun` returns 0, as FreeBSD only updates it when a signal is accepted - [The sandbox CA list](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) (libkernel), `//common/cert/CA_LIST.cer`, is a PEM bundle of the host's trust anchors, written once per process to `cert_cache/CA_LIST.cer` in the run directory with the owner write bit cleared: on Windows the current user's `ROOT` store, filtered through Disallowed and the combined EKU extension/property for TLS server authentication (Windows fetches missing roots on demand, so a root that was never fetched is absent), elsewhere `SSL_CERT_FILE` or the first of `/etc/ssl/certs/ca-certificates.crt`, `/etc/pki/tls/certs/ca-bundle.crt`, `/etc/ssl/ca-bundle.pem` and `/etc/ssl/cert.pem` that exists. On macOS, `/etc/ssl/cert.pem` is a static snapshot and does not reflect roots added to or distrusted in the Keychain. Existing guest files, explicit aliases and blocked mounts take precedence over the provider. The PEM snapshot does not carry Windows CTL restrictions beyond EKU, native revocation policy or later trust-store changes. Without any certificate the path does not exist. The console file's format is unverified: only that bundled OpenSSL accepts a PEM bundle there is shown (ProsperoRadio PPSA99001 and SwanStation PPSA98510, #5001). The other entries of the console's `common/cert` directory, the directory itself (it does not exist here) and the error a write open gets (`EACCES` from the host here) are unverified -- [pselect](../../../core/libs/prx/libkernel/Socket/src/Pselect.cpp) (libkernel) sets the signal mask with `sigprocmask`, calls `select` and restores the mask, as FreeBSD 11 `kern_pselect` does, but the swap is not atomic with the wait: a signal that becomes deliverable between the swap and the wait is not guaranteed to interrupt it +- [pselect](../../../core/libs/prx/libkernel/Socket/src/Pselect.cpp) (libkernel) sets the signal mask, calls `select` and restores the mask, as FreeBSD 11 `kern_pselect` does. A pending signal that the new mask unblocks has its handler run during the swap, and `pselect` then returns `EINTR` unless a descriptor is already ready, the timeout is zero or an argument is invalid, as FreeBSD's `kern_select` decides before it sleeps. A signal raised during the wait, including one sent by another thread, does not interrupt it, because `select` has no `EINTR` path; and the mask is the process-wide one, so it applies to every thread for the duration of the call - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them - [setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only -- [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - the answer for a thread that has ended is not measured on the console, and [`pthread_attr_get_np`](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) still returns 0 for such a thread, reading a detached one's object after it is freed. `scePthreadAttrGet` returns `ESRCH` (`0x80020003`) for a thread that has ended, joined or not and detached or not, as FreeBSD 11.4's [`_pthread_attr_get_np`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L139-L172) does through `_thr_find_thread`; a thread counts as ended once its thread-specific data destructors have run, where FreeBSD marks it `PS_DEAD`. On Windows a host thread adopted by `scePthreadSelf` that ends without `scePthreadExit` keeps answering 0. Monster Hunter Wilds (PPSA24191) reuses a slot of its thread table only when `scePthreadAttrGet` returns `0x80020003` +- [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the answer for a thread that has ended is not measured on the console; the implementation follows FreeBSD 11.4's [`_pthread_attr_get_np`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L139-L172) through `_thr_find_thread`. On Windows a host thread adopted by `scePthreadSelf` that ends without `scePthreadExit` keeps answering 0. Monster Hunter Wilds (PPSA24191) reuses a slot of its thread table only when `scePthreadAttrGet` returns `0x80020003` - [scePthreadAttrGet, pthread_attr_get_np and sceKernelIsStack](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) report the stack of a host thread the library did not create, on Windows, from one allocation granule (64 KiB) above the bottom of its reservation when everything above that is committed read-write, as in a relinked executable's main thread and its default-size host threads (0x200000 committed of 0x210000 reserved), so the guest can protect the lowest reported pages, as HotSpot does for its stack guard pages in Java 25 LAN / DNS (PPSA99869). Any other such thread, and every such thread on Linux, gets its whole host stack: on Windows `mprotect` commits its reserved lower pages before protecting them and drops the guard flag of Windows' guard pages, and on Linux it cannot change the unmapped lower pages - [sceKernelInstallExceptionHandler](../../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler, but a host fault still ends the process: only signals raised with `sceKernelRaiseException` are delivered. Unity's PS5Util module installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Linux sends the host SIGUSR1, which the library takes over, to the target thread, so the handler runs wherever the thread was interrupted, emulator code included. On the console kernel objects are system calls and libthr defers signals while a thread holds its internal locks; here a handler that needs a host lock the interrupted code holds deadlocks, and a thread that the handler keeps waiting keeps its host locks. A thread holding the address-wait lock (`sceKernelSyncOnAddressWait`, `sceKernelSyncOnAddressWake`, `_umtx_op`) records the signal and runs the handler once it releases the lock or while it waits; other emulator locks are not deferred. Host waits resume after the handler. The host calls Linux does not restart end early: sleeps that do not retry return before their time, and the poll of `sceNetEpollWait` and socket calls with a send or receive timeout return `EINTR` to the guest - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows suspends the target and redirects it to the handler. A target running host code (any module other than the executable and the `*.guest.prx` modules, so libkernel, libc and the other HLE modules count too) is resumed and sampled again until it is back in guest code, since a handler that waits there would keep the host locks that code holds. The raising thread spins for 20 µs before each new host-code sample: suspended again at once, the target could stay at the same instruction for every sample until the 50 ms below ran out (on a 16-core Ryzen 9 5950X, `guest_raise_in_host_code` then raised into `fwrite` and deadlocked on its stream lock), and the 20 µs is a measured choice, not a console value; a target blocked in an ntdll wait is redirected in place, as are NtContinue and winpthreads after their retry limit. A target that stays in host code without waiting for 50 ms after the raise began, such as a long HLE or driver call, is then redirected in host code anyway, with the same hazard. A target seen right after a `syscall` instruction in ntdll, inside a system call that is not a wait such as the `NtWriteFile` of an `fwrite` holding its stream lock, restarts those 50 ms, so a raise can cost the raising thread up to the 1 s retry limit. While the target is suspended only `VirtualQuery` and a lock-free cache of image bases are consulted; an image not yet in the cache is classified with `GetModuleFileNameW` after the target is resumed. An APC queued at the first host-code sample, which an alertable guest wait would run sooner, is not implemented - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) builds the handler's `ucontext` with FreeBSD's amd64 layout (16-byte sigmask, `mcontext` at 0x40, `mc_rsp` at 0xf8); the console's layout is not verified beyond the fields PS5Util reads - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) does not consult the target thread's signal mask -- [pthread_sigmask](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) shares the process-wide mask of `sigprocmask` and `_sigprocmask`, which keeps only the first 32 signals; FreeBSD keeps a mask per thread +- [pthread_sigmask](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) shares the process-wide mask of `sigprocmask` and `_sigprocmask`; FreeBSD keeps a mask per thread - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) reaches a thread that waits in an alertable `TimedWait` wait through an APC. A thread blocked anywhere else, including all waits under `APS5_COARSE_TIMED_WAITS` and host waits inside the emulator, is redirected and runs the handler only when that wait returns - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs the handler wherever the target thread was interrupted. It waits only while the thread is inside winpthreads or restoring a context through `NtContinue`, and throws if that lasts 1 s, so a handler that calls into other host code the thread was interrupted in, such as the C runtime's heap or a lock of the emulator, can deadlock on it or reenter it. A thread that holds the address-wait lock of `sceKernelSyncOnAddressWait`, `sceKernelSyncOnAddressWake` and `_umtx_op`, or the park queue lock of the guest mutexes, gets the raise as an APC instead, which runs once it has released the lock or while it waits; the other locks of the emulator are not deferred this way +- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows throws for a target thread that was created suspended and is not resumed yet. FreeBSD 11.4 libthr starts such a thread with every signal but `SIGTRAP` blocked ([`thr_create.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_create.c#L177-L183)) and restores its mask only once it is resumed ([`thread_start`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_create.c#L252-L294)), so a signal sent to it stays pending until then. The console's behaviour is unverified - [sigaltstack](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records each thread's alternate signal stack and reports it back, following FreeBSD's `kern_sigaltstack` (`EINVAL` for flags other than `SS_DISABLE`, `ENOMEM` below `MINSIGSTKSZ` 2048), but handlers never run on it, so `SS_ONSTACK` and `EPERM` are never reported - [sigpending](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) reports the intersection of the process-wide pending set of `sigtimedwait` and the blocked mask of `_sigprocmask`, as FreeBSD's `sys_sigpending` intersects the pending list with `td_sigmask`, and returns -1 with `EFAULT` for a null set. PPSA99420 imports it - [scePthreadJoin, scePthreadCreate](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - a joined thread is kept until `scePthreadCreate` publishes over the same handle storage, so one host object and its handle are retained per distinct guest storage location for the life of the process; a guest that joins into a fresh location every time keeps every one. A handle copied to a second storage and joined there retains the object under the original storage key, so a later `scePthreadCreate` over the original storage releases an object the copy still points at, and the copy reads freed memory. A detached thread is released when it ends, so joining it after it ended reads freed memory. A null entry crashed the payload and throws. Joining the calling thread returns `EDEADLK` and a zeroed attribute creates a default thread, as on the console. PPSA34015 once stopped with `Joining guest thread: Access is denied` before the join and detach checks followed the console @@ -190,25 +204,24 @@ - [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 - [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 - [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](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns -1 with `ECHILD` (`EINVAL` for unknown options): nothing here creates a child process; child processes would need a real `waitpid`. -- [execv](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported, the same as `execvp`. PPSA97358 imports it and fails to load without it. -- [execve](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws like `execvp`: replacing the process image is not supported. The console behavior for invalid arguments and executable path lookups is not modeled. +- [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`. +- [execv, execvp and execve](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) replace the running image with the converted guest executable, following FreeBSD 11.4 `exec_copyin_args`: a null path is `EBADF` (measured), a null `argv` `EFAULT`, an empty path `ENOENT`; an empty `argv` and a null or unpaired `envp` are copied, `E2BIG` on their bytes; a lookup misses with `ENOENT`, a path over a regular file with `ENOTDIR`, and a regular file with no execute permission `EACCES` (0644 measured); `execvp` walks `PATH` skipping non-regular and inexecutable candidates, keeping their `EACCES`, or ends with `ENOENT`. Linux replaces through `fexecve`, returning `ENOEXEC` (measured) for a non-x86-64 target and `EACCES` for one without execute permission; Windows probes the PE header for the same `ENOEXEC`. A missing converted artifact throws. Windows execute permission is libc `access`'s ACL. The replacement keeps the host `environ`, `execve`'s `envp` name overrides - [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `AMD64_GET_FSBASE`, `AMD64_GET_GSBASE` and `AMD64_SET_GSBASE` follow FreeBSD 11's amd64 [`sysarch`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c) on Linux x86-64 hosts, through the host's `arch_prctl`. The GS base belongs to the calling thread, a base at or above `0x800000000000` (`VM_MAXUSER_ADDRESS`) returns `EINVAL`, and a null argument returns `EFAULT`. `AMD64_SET_FSBASE` throws, since guest and host code share the thread's FS base for their TLS. In a payload process on a PS5 (firmware 12.70), `I386_GET_FSBASE` (7) and `I386_GET_GSBASE` (9) wrote 4 bytes, the low 32 bits of the FS and GS bases, and failed with `EFAULT` for a null argument; here they do the same on Linux x86-64 hosts. There a GS base set by one thread left the main thread's unchanged, and threads made with `pthread_create` and `scePthreadCreate` started with their creator's GS base; here a guest thread's host thread is created by the creating thread, and Linux's [`copy_thread`](https://github.com/torvalds/linux/blob/v6.6/arch/x86/kernel/process.c#L182-L186) gives a new thread its creator's GS base. The operation numbers FreeBSD 11's amd64 `sysarch` has no case for returned `EINVAL` there for the nine tried, each with a null argument (2, 5, 6, 12, 127, 133, 134, 1000 and -1; 6 is `I386_VM86`, which its header names), as its [default case](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c#L324-L325) does; here every such number returns `EINVAL` on every host; the numbers other than those nine follow that default case and were not measured. `AMD64_GET_XFPUSTATE` (132) returned 0 there for lengths 0, 64, 128, 256 and 512 (512 was also the largest length a search accepted) and failed with `EINVAL` for 1024, 2048, 4096 and 8192 and with `EFAULT` for a null argument or address, and `I386_GET_XFPUSTATE` (11) failed with `EFAULT` for a null argument; both throw here. The other operations FreeBSD 11's amd64 `sysarch` handles, and every operation it handles on other hosts (Windows keeps its TEB in GS), throw. - [kern.proc.pathname](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. - [kern.proc.env](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - which variables, if any, the console starts an application with is unverified; `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_ENV`, pid} returns 0 bytes, the empty start-up environment `environ` and libc's `getenv` also begin with, as FreeBSD 11's [`sysctl_kern_proc_env`](https://github.com/freebsd/freebsd-src/blob/releng/11.0/sys/kern/kern_proc.c) copies a process's start-up strings, not later `setenv` changes. Only the calling process's pid is accepted (`ESRCH` otherwise, -1 included); a write returns `EPERM`. PS5VR (PPSA99177) reads it for its own pid at start-up. - [kern.proc.vmmap](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_VMMAP`, pid} returns packed FreeBSD 11 `kinfo_vmentry` records (144 bytes each, an empty path) for the used ranges of the guest address-space arena, in address order, as [`kinfo_getvmmap`](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libutil/kinfo_getvmmap.c) reads them. A range is one record: the type is always `KVME_TYPE_DEFAULT`, the protection is `sceKernelVirtualQuery`'s for the range's first page (0 where it reports none), and offset, object, resident page counts, flags and path are 0. Only the calling process's pid is accepted (`ESRCH` otherwise, -1 included, as FreeBSD's `pget` gives); a write returns `EPERM` and a short buffer `ENOMEM`. Ranges outside the arena, which guest code cannot map, are not reported. The walk takes the arena lock once per range, not for the whole walk as FreeBSD holds the map lock, so an `mmap` or `munmap` on another thread can change the list between two records. Whether a PS5 application may read its own map, and what types and paths the console reports, is unverified -- [sysctl names measured absent on a PS5 payload](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - On firmware 12.70, a payload gets `ENOENT` for `hw.byteorder`, `hw.cacheline_size`, `hw.machine_arch`, `hw.usermem`, `kern.clockrate`, `kern.maxfiles`, `kern.maxfilesperproc`, `kern.maxproc`, `kern.ps_strings`, `kern.sched.name`, `kern.smp.maxcpus`, `kern.stackprot`, `kern.timecounter.hardware` and `machdep.cacheline_size`. `sysctl` returns `ENOENT` for MIBs {1, 6}, {1, 7}, {1, 12}, {1, 27}, {1, 32}, {6, 4} and {6, 11}; reads and size queries leave output buffers and lengths untouched. `sysctlnametomib` resolves the seven corresponding MIB names and returns `ENOENT` for `hw.cacheline_size`, `hw.usermem` and `machdep.cacheline_size`. Writes and title behavior are unmeasured. -- [Unknown sysctl names](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - A payload process on firmware 12.70 gets `ENOENT` for `kern.anyps5_no_such_name` through `sysctlbyname` and `sysctlnametomib`; here both name-based calls return `ENOENT` for unrecognized names, following FreeBSD name lookup. This is one payload measurement; title behavior for other unknown names is unverified. Numeric MIBs with no implementation still throw. -- [hw.pagesize](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the system page size (16384 bytes, PS5_PAGE_SIZE) as an integer through sysctl MIB {6, 7} (CTL_HW, HW_PAGESIZE) and the "hw.pagesize" sysctlbyname lookup, following FreeBSD's [`hw.pagesize`](https://github.com/freebsd/freebsd-src/blob/main/sys/kern/kern_mib.c#L177), a read-only `SYSCTL_INT` of `PAGE_SIZE`. The value on a PS5 follows the 16 KiB page size the rest of libkernel uses and is unmeasured. A write returns -1 with EPERM. -- [hw.model](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the [host CPU's name](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp), built as FreeBSD 11 builds `hw.model`: the CPUID brand string (leaves 0x80000002 to 0x80000004) without leading spaces ([identcpu.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/identcpu.c#L683-L691)). Trailing padding stays, as it does there, so an AMD host's name ends in spaces. The name a PS5 reports is unknown. A write returns -1 with `EPERM`. A host without an x86 CPUID brand string, such as an Apple Silicon Mac, throws. PPSA99203 only prints it (`[console] hw.model: %s`) +- [sysctl names a PS5 payload process gets ENOENT for](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - a write to these names throws (except `machdep.cacheline_size`, `ENOENT` as on FreeBSD amd64), `sysctlnametomib` returns `ENOENT` for `kern.smp.maxcpus`, `kern.stackprot`, `kern.sched.name` and `kern.timecounter.hardware`, which the console maps to MIBs above 2147482800; a title process was not measured. A payload process on a PS5 (firmware 12.70, and 13.60 for four names) gets -1 with `ENOENT` by name for `hw.byteorder`, `hw.cacheline_size`, `hw.floatingpoint`, `hw.machine_arch`, `hw.usermem`, `kern.bootfile`, `kern.clockrate`, `kern.cp_time`, `kern.ident`, `kern.maxfiles`, `kern.maxfilesperproc`, `kern.maxproc`, `kern.ps_strings` and those four, and by MIB for {1, 6}, {1, 7}, {1, 12}, {1, 27}, {1, 32}, {6, 4} and {6, 11}, as here. `hw.physmem`, `hw.realmem` and `vm.overcommit` report values here +- [Unknown sysctl names and MIBs](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - a numeric MIB under a known top-level node, or under one of 256 or more (where FreeBSD numbers dynamic nodes), that is not implemented throws, where a payload process on PS5 firmware 13.60 gets `ENOENT` for {1, 9999}, {6, 9999}, {0, 9999}, {1, 14, 9999, pid} and 24 levels starting {1, 9999}, since an unimplemented MIB here may exist there. A top-level node below 0 or from 10 to 255 gets `ENOENT` with the buffer and length untouched, as {99, 1} did there. Unrecognized names get `ENOENT` through `sysctlbyname` and `sysctlnametomib`, as `kern.anyps5.x` did. The ten FreeBSD top-level names (`sysctl` to `p1003_1b`), with or without a trailing dot, map to one level, and `sysctlbyname` fails them with `EINVAL`; a name of 1024 bytes or more gets `ENAMETOOLONG` from both, as measured for `kern` and `kern.` +- [hw.pagesize](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the system page size (16384 bytes, PS5_PAGE_SIZE) as an integer through sysctl MIB {6, 7} (CTL_HW, HW_PAGESIZE) and the "hw.pagesize" sysctlbyname lookup, following FreeBSD's [`hw.pagesize`](https://github.com/freebsd/freebsd-src/blob/main/sys/kern/kern_mib.c#L177), a read-only `SYSCTL_INT` of `PAGE_SIZE`. A payload process on a PS5 (firmware 13.60) read the same 4-byte 16384 by name and by MIB. A write returns -1 with EPERM. - [Host CPU check](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp) (libkernel) - when libkernel loads on a Linux or Windows x86-64 host, it logs which of the PS5 CPU's user-mode extensions CPUID reports and throws, naming the host CPU, if any extension is missing that LLVM selects from plain C/C++ for the PS5's Zen 2 (`-march=znver2`, [feature list](https://github.com/llvm/llvm-project/blob/llvmorg-19.1.7/llvm/lib/Target/X86/X86.td#L1459-L1513)): SSE to SSE4.2, SSSE3, POPCNT, LZCNT, BMI1, BMI2, MOVBE, CMPXCHG16B, LAHF/SAHF, AVX, AVX2, FMA, F16C, and the operating system's AVX state in XCR0 (#3719). SSE4A, CLZERO and SHA, which `--to-intel` converts, and AES, PCLMULQDQ, RDRAND, RDSEED, ADX, CLFLUSHOPT, CLWB, RDPID, XSAVEOPT, XSAVEC and PREFETCHW, which compilers emit only for intrinsics or explicit prefetches, are logged but not required: a title that executes one of them on a host without it still stops with an illegal instruction. macOS hosts are not checked: some Rosetta versions execute extensions without reporting them through CPUID, but support differs by host. ANDN and RDRAND fault on an M1 with macOS 15.3.1; see the [Rosetta conversion limits](relinker.md) - [sceKernelGetSystemExVersion, sysKernelGetLowerLimitSysexVersion, sysKernelGetLowerLimitUpdVersion](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) and [sceKernelGetTraceMemoryStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - follow libkernel.sprx, libkernel_sys.sprx and libkernel_web.sprx of firmware 13.42, read from disassembly and not measured on a console. None of them asks the kernel: the first writes an empty string to both outputs and returns a plain 22, not `SCE_KERNEL_ERROR_EINVAL`, when either is null; the lower limits write an empty string and a 32-bit 0; the trace memory statistics write 0 to each output that is not null. The meaning of the outputs is not known - [machdep.tsc_freq](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the host TSC frequency that `sceKernelGetTscFrequency` measures, as a 64-bit value like FreeBSD 11's [`sysctl_machdep_tsc_freq`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/tsc.c#L658-L676). FreeBSD lets root write it to change the TSC timecounter's frequency; a write here returns -1 with `EPERM`. PPSA99203 reads it at start-up and measures the TSC itself when the value is 100 MHz or less - [getrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `RLIMIT_CPU`, `RLIMIT_FSIZE`, `RLIMIT_CORE`, `RLIMIT_SBSIZE`, `RLIMIT_VMEM`/`AS`, `RLIMIT_NPTS`, `RLIMIT_SWAP`, `RLIMIT_KQUEUES` and `RLIMIT_UMTXP` report infinity as in FreeBSD's process-0 defaults, and `RLIMIT_DATA` mirrors the host limit; a payload process on a PS5 (firmware 12.70) reads 0x800000000 for both limits. `RLIMIT_NOFILE` reports, as both soft and hard limit, the number of descriptors after which AnyPS5 itself fails with `EMFILE`: on Windows the C runtime's 8192 low-level descriptors, which guest files, pipes and sockets all take, and as many stdio streams, which libkernel raises from 512 at load with [`_setmaxstdio`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setmaxstdio), on Linux the host's `RLIMIT_NOFILE`, whose soft limit libkernel raises to the hard limit at load, and on macOS the smaller of the host's hard limit and `kern.maxfilesperproc`, which libkernel sets as both limits at load, since macOS fails with `EMFILE` at `kern.maxfilesperproc` whatever the soft limit. A payload process on a PS5 (firmware 12.70) reads 13952 for both limits, while `getdtablesize` returns 12555 and the process can open descriptors up to 12554. XPSemu (PPSA97358) reads `RLIMIT_NOFILE` in QEMU's `os_setup_limits` and raises the soft limit to the hard one when they differ. `RLIMIT_STACK` (0x200000), `RLIMIT_RSS` and `RLIMIT_MEMLOCK` (infinity) and `RLIMIT_NPROC` (1604) report the soft and hard limits a payload process reads on a PS5 (firmware 12.70). Guest locking still follows the host: `mlock` and `sceKernelMlock` fail at the host's hard `RLIMIT_MEMLOCK` on Linux, and on Windows when the process working set cannot grow, although `RLIMIT_MEMLOCK` reports infinity. There the main thread's stack is 0x200000 as well (`pthread_attr_get_np`); here the main thread runs on the host thread's stack, whose size can differ. Whether a title process reads the same limits is unverified. +- [EVFILT_TIMER modify with data 0](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - a timer change with `EV_ADD`, `data` and `fflags` unset keeps the note's period instead of taking FreeBSD's zero default, which would fire without delay; whether the console keeps or zeroes the period is unverified. `EV_ENABLE` on a disabled timer reports the expiries it missed while disabled, as FreeBSD 11's `filt_timerexpire` counts a disabled note and keeps its phase: the missed count is summed in one step at the first wait and the next expiry keeps the note's phase, which `TimerDisableEnableRearm` pins; not measured on a console. A change that sets `EV_ADD`, `data` or `fflags` still throws, waiting for the measured re-add rules: FreeBSD 11's `kqueue_register` copies `data` and `fflags` onto the note and the new period applies at the next expiry. - [setrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) validates the resource and input in FreeBSD order, maps negative limits to infinity, and lowers a soft limit above the hard limit. A request other than `RLIMIT_NPROC` succeeds when its normalized values match `getrlimit`, or, for `RLIMIT_NOFILE`, when `rlim_max` does not exceed the current hard limit and `rlim_cur` does not exceed `rlim_max`, storing the updated limits for subsequent `getrlimit` queries; descriptor allocation enforcement of the lowered soft limit is not implemented, and changing other limits throws because guest limit state is not stored. In a payload process on a PS5 (firmware 12.70), `setrlimit(RLIMIT_NOFILE)` lowers a requested 13952 to 12555, as the hard and as the soft limit; here a request within the current hard limit is stored as given and one above it throws; setting `RLIMIT_STACK`, `RLIMIT_RSS` and `RLIMIT_MEMLOCK` to their current values succeeds and changes nothing, as here; setting `RLIMIT_NPROC` to its current 1604 also succeeds but leaves 1443. `RLIMIT_NPROC` follows FreeBSD 11 by clamping both values to 1 through `maxprocperuid`, stores the result and returns it from `getrlimit`. The initial 1604 and `maxprocperuid` 1443 follow that measurement: process 0's limit starts at `maxproc` ([`init_main.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/init_main.c#L550-L551)), `maxprocperuid` starts at nine tenths of it ([`subr_param.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/subr_param.c#L258)), and `setrlimit` applies that bound ([`kern_resource.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_resource.c#L714-L718)). `kern.maxproc` fails with `ENOENT` in the payload process, so the values cannot be queried independently, and process creation does not enforce the stored limit. Java 25 LAN / DNS (PPSA99869) sets `rlim_cur = MIN(0x100000, rlim_max)` during VM initialization -- [execvp](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported. PPSA85300 imports it and fails to load without it; its `fork` and `vfork` fail with `ENOSYS` ([libc_missing.c](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/ps5/compat/libc_missing.c#L290-L292)), so the child process that would call it is never created. +- [socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) turns off Winsock's report of an ICMP port-unreachable reply on datagram sockets (`SIO_UDP_CONNRESET`). Winsock fails the next receive on any datagram socket with `WSAECONNRESET`, where FreeBSD's [`udp_common_ctlinput`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/netinet/udp_usrreq.c#L774-L798) reports the reply only to a socket connected to the sender. So on Windows a datagram socket connected to a port nobody listens on does not fail its next send or receive with `ECONNREFUSED` either - [nmount](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throws: mounting file systems is not supported. Prospero Themes (PPSA90210) imports it and fails to load without it; it opens its theme library without calling it, and its strings (`/dev/ssd0.system_ex`, `/system_ex`, `exfatfs`, `fstype`, `fspath`, `from`, `ignoreacl`) point to remounting the console's `/system_ex` partition to write theme files there. -- [socketpair](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and macOS and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows and macOS, which has no such `AF_UNIX` pair. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and the first stream `send` after the peer closed succeeds on the loopback connection, where the console returns `EPIPE`; the sends after it report `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, as the console does after a `shutdown(SHUT_WR)`; after the peer's `close` the console reports `POLLIN` with `POLLHUP`, because `unp_drop` disconnects the other end, and the loopback connection cannot tell that from a shutdown, so no `POLLHUP` is reported +- [socketpair](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and macOS and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows and macOS, which has no such `AF_UNIX` pair. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and the first stream `send` after the peer closed succeeds on the loopback connection, where the console returns `EPIPE`; the sends after it report `EPIPE`. `read` and `write` on a pair end go to the host as on every guest socket. `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, as the console does after a `shutdown(SHUT_WR)`; after the peer's `close` the console reports `POLLIN` with `POLLHUP`, because `unp_drop` disconnects the other end, and the loopback connection cannot tell that from a shutdown, so no `POLLHUP` is reported - Guest sockets on macOS ([the equeue wait, kevent, fstat and ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp), [gethostbyname](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp)): macOS has no `ppoll`, so the wait uses `poll` with the timeout rounded up to a millisecond; it has no `POLLRDHUP`, so a stream that the peer closed is found by peeking one byte when it is readable; `FIONREAD` on a datagram socket, and the byte count that `fstat` and `kevent` report for it, is `SO_NREAD`, the size of the next datagram, because macOS adds the source address to `FIONREAD`; and `gethostbyname` replaces `gethostbyname_r`, which macOS lacks. - [recvmsg](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) loses received stream data on an output copy fault: outputs are checked after the host receive. FreeBSD 11.4 leaves unread stream data queued, though it also drops a datagram on a copy fault ([`soreceive_dgram`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L2308-L2312), [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1758-L1774)). A Linux `process_vm_readv` or `process_vm_writev` failure other than `EFAULT` throws - [MSG_DONTWAIT](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `send`, `sendto`, `sendmsg`, `recv`, `recvfrom` and `recvmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`). Winsock has no per-call non-blocking flag, so on Windows the call first asks `WSAPoll` with a zero timeout whether the socket is ready and fails with `EAGAIN` when it is not. The check and the transfer are two host calls: a second thread that takes the data in between can leave a `recv` blocked, and a `send` of more bytes than the socket buffer holds can still block until it has sent them, where FreeBSD's `sosend_generic` returns a partial count @@ -216,20 +229,23 @@ - [recv and recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answer some cases without the host. A zero-length receive on a stream socket returns 0, or `ENOTCONN` when it is not connected ([L1566-L1578](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1566-L1578)), as on a PS5 (firmware 13.60). On an empty datagram socket it returns 0 at once; the console was measured only with `MSG_DONTWAIT`, and the blocking case follows FreeBSD 11.4's `soreceive_dgram`, which returns 0 before waiting. `recvmsg` answers the empty datagram socket the same way but asks the host in the stream cases. A datagram another thread takes after the check makes a zero-length receive wait - [recv and recvfrom after `shutdown(SHUT_RD)`](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) return 0 on Windows ([`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1556-L1562)) where Winsock fails with `EPIPE`; Linux and macOS still return data buffered before it, which the console discards. - [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) check a socket address length as FreeBSD 11.4's [`getsockaddr`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_syscalls.c) and the `sa_len` test of `tcp_usr_connect` and `in_pcbconnect` do: below 2 or an `AF_INET` address below 16 or an `AF_INET6` address below 28 bytes fails with `EINVAL`, above 255 with `ENAMETOOLONG`. `sendto` does not check an address longer than `sockaddr_in` on an `AF_INET` datagram socket, where `udp_output` answers `EINVAL` unless the socket has a local address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, nor on an unconnected stream socket, which FreeBSD connects implicitly through `tcp_connect` and its length test. An address of another family still fails with `EAFNOSUPPORT`, as the console's behaviour for it is not known. None of this was measured on a console. +- [bind, connect, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) check a socket address as a payload process on a PS5 (firmware 13.60) did on datagram sockets: a length below 2 fails with `EINVAL` and above 255 with `ENAMETOOLONG`; an `AF_INET` socket needs exactly 16 bytes and an `AF_INET6` socket 28, or `EINVAL`; another family fails with `EAFNOSUPPORT`; `sendto` on an `AF_INET` socket, and all three on an `AF_INET6` socket, check the length first. Unmeasured: `bind` and `connect` on an `AF_INET` socket check the family first, where FreeBSD 11.4's `tcp_usr_bind` and `tcp_usr_connect` check the length first; stream sockets follow the datagram order; `AF_UNSPEC` with `INADDR_ANY`, which FreeBSD's `bind` accepts, fails; `connect` on a listening stream socket fails with `EOPNOTSUPP` first, as `soconnect` does. A connected stream socket's `sendto` ignores the address once its length passes, as `tcp_usr_send` does +- [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 -- [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` -- [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. -- [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. Open descriptors 0 to 2 follow the console model `select` uses: descriptor 0 never reports an event and descriptors 1 and 2 report `POLLOUT`, whatever the host attaches to them. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket, a pipe, a file, a directory, the random device nor descriptor 0 to 2 (other devices) 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`; the console's limit on the descriptor count is unknown and not checked +- [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 reports st_dev NODEV and, from its first fstat, a nonzero st_ino it keeps, as FreeBSD's uipc_sense does, but the number comes from a counter this process starts at 1, where FreeBSD's unp_ino is 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`; the console's limit on the descriptor count is unknown and not checked - [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) on Windows reports a guest stream socket whose peer closed as readable and adds `POLLHUP` only once the socket has shut its own sending side, as FreeBSD 11.4's [`sopoll_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3080-L3086) does; `WSAPoll` reports `POLLHUP` alone in both cases. A reset or refused connection (`POLLHUP` with `POLLERR`) keeps `WSAPoll`'s flags, a libSceNet socket keeps them too, and a call that asks for no event still returns early on the peer's close with nothing reported. Not measured on a console. +- [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 on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - host pipes are one-way, where FreeBSD's [`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422) opens both ends for reading and writing, so `EVFILT_READ` on the write end and `EVFILT_WRITE` on the read end throw; on macOS `EVFILT_WRITE` on a pipe throws, as the host has no call for a pipe's capacity. `EVFILT_WRITE` reports the free space of the host buffer, not of FreeBSD's 16 KiB one, and on Linux fires when the host says the pipe is writable rather than at `PIPE_BUF` (512) free bytes. Windows pipes cannot be waited on, so a kqueue waiting on one checks it every 1 ms. Otherwise [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1828) and `filt_pipewrite` are followed: `EV_EOF` once the other end closes, `EPIPE` for a write event added after the read end closed, and `NOTE_LOWAT` ignored. On Windows `fcntl` cannot make a pipe write end non-blocking, so the full-pipe and drain case of `EVFILT_WRITE` is tested on Linux only and the `NtQueryInformationFile` write quota is checked there only for the free space, `EV_EOF` and `EPIPE`. - [kevent on a kqueue](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on a kqueue counts the watched queue's enabled notes that are ready when it is evaluated, where [`filt_kqueue`](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/sys/kern/kern_event.c#L363-L370) reports `kq_count`, the queued notes: a queued note counts until that kqueue is scanned, even once disabled or no longer ready (a socket or pipe read empty, a user note cleared, a watched kqueue drained), so FreeBSD can report it readable where this reports nothing. A queued `EV_ONESHOT` note is reported there with its last count and deleted; here it is evaluated again and, if the watched queue is empty by then, neither reported nor deleted. `EV_CLEAR` throws, as FreeBSD re-arms it only when the watched kqueue queues a note. A cycle of watches throws; a note on the queue itself is `EINVAL`. Not measured on a console - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 -- [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - a guest socket has the SOL_SOCKET level, five `IPPROTO_TCP` options, `IPV6_V6ONLY` and seven `IPPROTO_IP` options. `TCP_NOPUSH`, `TCP_NOOPT` and `TCP_KEEPINIT` fail with `ENOPROTOOPT` (42), where a payload process on a PS5 (firmware 13.60) read them as 0, and so does `SO_ACCEPTFILTER` (0x1000), which the console fails with `EINVAL` for a set and a get on a non-listening socket, as FreeBSD 11.4 does; the other options of these levels fail with `ENOPROTOOPT` and were not all measured. `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and a boolean one reads back as its own bit, as there. Winsock does not clear a pending error when `SO_ERROR` reads it, so on Windows a pending error is reported once and again only after a new `connect`; a second failure with the same code without a `connect` in between is hidden +- [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - a guest socket has the SOL_SOCKET level, eight `IPPROTO_TCP` options, `IPV6_V6ONLY` and ten `IPPROTO_IP` options. `SO_ACCEPTFILTER` (0x1000) fails with `ENOPROTOOPT` (42), where a payload process on a PS5 (firmware 13.60) failed it with `EINVAL` for a set and a get on a non-listening socket, as FreeBSD 11.4 does; the other options of these levels fail with `ENOPROTOOPT` and were not all measured. `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and a boolean one reads back as its own bit, as there. Winsock does not clear a pending error when `SO_ERROR` reads it, so on Windows a pending error is reported once and again only after a new `connect`; a second failure with the same code without a `connect` in between is hidden +- [TCP_NOPUSH, TCP_NOOPT, TCP option 0x80, IP_RECVDSTADDR, IP_RECVIF, IP_RECVTTL](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept and read back as a payload process on a PS5 (firmware 13.60) did: the TCP ones as the bits 0x1000, 0x8 and 0x10000 (so 0x80 is not FreeBSD's `TCP_KEEPINIT` there), the IP ones as 0 or 1. None of them changes what the socket does: `TCP_NOPUSH` does not hold back sends, and the IP ones add no control message to `recvmsg`. `IP_RECVTTL` was only read, never set, on the console, and an accepted socket takes `TCP_NOPUSH` from its listener as FreeBSD 11.4's `syncache_socket` does, which was not measured - [SO_DEBUG, SO_REUSEADDR, SO_USELOOPBACK, SO_REUSEPORT, SO_TIMESTAMP, SO_BINTIME and option 0x10000](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept as bits that read back as on a PS5 (firmware 13.60, payload process), and options 0x4000 and 0x8000 are accepted and cannot be read, as there; only `SO_REUSEADDR` and `SO_REUSEPORT` change what the socket does. Both are set on the host, `SO_REUSEPORT` as `SO_REUSEADDR` on Windows, which has no `SO_REUSEPORT` and there also lets the socket bind over one that did not set it; Linux spreads unicast datagrams across the sockets, where FreeBSD gives each to one. Option 0x10000 is only kept: if the console treats it as FreeBSD 12's `SO_REUSEPORT_LB`, two sockets that set it share a port there, where the second `bind` here fails with `EADDRINUSE`. An accepted socket takes the bits from its listener, as FreeBSD 11.4's `sonewconn` copies `so_options`; that was not measured - [IP_ADD_MEMBERSHIP, IP_DROP_MEMBERSHIP, IP_MULTICAST_IF](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - set on the host socket after the checks a PS5 (firmware 13.60, payload process) showed: `EOPNOTSUPP` on a stream socket, `EINVAL` for a short value or a group outside 224.0.0.0/4, `EADDRINUSE` for a group joined twice and `EADDRNOTAVAIL` for one not joined, tracked per socket. An interface the host lacks fails with the host's error (`EADDRNOTAVAIL` on the console). A group joined through `INADDR_ANY` and through that interface's address counts as two here. Not measured, after FreeBSD 11.4: a drop through `INADDR_ANY` leaves the first such group on any interface (`inp_leave_group`), and the other multicast options fail with `EOPNOTSUPP` on a stream socket (`inp_setmoptions`). An `ip_mreqn` with a non-zero index or address throws, as only a zeroed one was measured, and so does reading `IP_MULTICAST_IF` into 12 bytes, where FreeBSD returns an `ip_mreqn` -- [TCP_MAXSEG](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per stream socket, read as 536 at first and failing with `EINVAL` for 1000, 70000 and 100, as a payload process on a PS5 (firmware 13.60) did. No segment set succeeded there, so the rule here, `EINVAL` above the current segment or below 176 and any segment between accepted, follows FreeBSD 11.4's `tcp_default_ctloutput` (`optval + 40 >= V_tcp_minmss`) and is not measured. An `AF_INET6` socket also starts at 536 here, where FreeBSD's `tcp_newtcpcb` uses 1220; not measured. The segment is not set on the host socket, so the host's segment size is its own, and a connected socket still reads the segment it was given instead of the negotiated one +- [TCP_MAXSEG](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - a connected socket reads 536 (1220 for `AF_INET6`) until a segment is set, where a payload process on PS5 firmware 13.60 read the negotiated segment (16344 over loopback, lo0's MTU less 40) on both ends; the segment is not set on the host socket, and a listening socket was not measured. Otherwise it follows that console: a fresh socket reads 536, or 1220 for `AF_INET6`, and fails every set with `EINVAL` (175, 176, 200, 400, 536, 537, 1460 tried); a connected one fails a segment above the current one or below 176, and stores the rest raised to 216 (176 and 200 both read back 216), as FreeBSD 11.4's `tcp_default_ctloutput` with `V_tcp_minmss` does - [TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per stream socket and read back as a payload process on a PS5 (firmware 13.60) did: 7200, 75 and 8 at first, 0 restoring a keepalive default, and a count stored as an unsigned value. They are not set on the host socket, so the host's keepalive timers are its own while `SO_KEEPALIVE` is applied there. An interval above 4294967 seconds fails with `EINVAL`, after FreeBSD 11.4's `tcp_default_ctloutput` with `hz` taken as 1000, and an accepted socket takes the keepalive values from its listener as `syncache_socket` copies them; neither was measured - [IP_TTL, IP_TOS, IP_MULTICAST_TTL, IP_MULTICAST_LOOP](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per `AF_INET` socket with the defaults (64, 0, 1, 1), the low-byte storage of `IP_TTL` and `IP_TOS`, the one-byte forms and the `EINVAL` above 255 of the multicast options that a payload process on a PS5 (firmware 13.60) showed. `IP_TTL` and the multicast options are also set on the host socket, except a time to live of 0, which hosts refuse; `IP_TOS` is only kept, so the host's packets carry its own type of service, and the host's default time to live (128 on Windows) is not changed to 64. An accepted socket takes `IP_TTL` and `IP_TOS` from its listener, as FreeBSD's `syncache_socket` does, and an `int` `IP_MULTICAST_LOOP` keeps only its low byte, as FreeBSD 11.4's `inp_setmoptions` does; neither was measured. Winsock applies `IP_MULTICAST_LOOP` on the receiving socket, not the sender, so on Windows the receiver's setting decides loopback - [SO_SNDBUF, SO_RCVBUF, SO_SNDLOWAT, SO_RCVLOWAT](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket with the defaults, the 524288-byte limit, the `ENOBUFS` above 5592405 bytes and the low-water clipping of a payload process on a PS5 (firmware 13.60); the host socket gets the size asked for, so a request above the limit still enlarges the host's buffer. The host's real buffering, FreeBSD's automatic growth of a connected TCP socket's buffers, and a low-water mark's effect on `select`, `poll` and a receive are not reproduced. An accepted socket takes all four from its listener, as FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) does; that was not measured @@ -237,31 +253,31 @@ - [SO_SNDTIMEO, SO_RCVTIMEO](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - the value checks and the 32.32 fixed-point read-back follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689), as a payload process on a PS5 (firmware 13.60) answered them. A receive or send on a blocking socket waits that long for the host to report the socket readable or writable and fails with `EWOULDBLOCK`, but only before the first transfer: once writable the host send has no limit, where FreeBSD returns the bytes sent so far, and a receive whose data another thread takes after the wait blocks again. `sendfile` ignores the send timeout, the host wait has millisecond steps, and an accepted socket takes both from its listener - [IPV6_V6ONLY](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - the `IPPROTO_IPV6` option 27 is set on the host socket and reads back as 0 or 1, after FreeBSD's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1716-L1732): a value that is not exactly an `int` fails with `EINVAL`, as does setting it on a bound socket, which the host refuses too. `socket` turns it on for every `AF_INET6` socket, as a payload process on a PS5 (firmware 13.60) reported, and Java turns it off on the sockets it opens: Java 25 Advanced Network (PPSA99871) stops at `java.net.SocketException: Protocol not available` without it. The value checks and the bound-socket answer were not measured - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux and on Windows a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again, and `getsockname` reports that host address. On Windows a `sendto` to a non-loopback address from such a socket then fails with `ENETUNREACH` (Winsock 10051, measured on Windows 10), where FreeBSD sends it from the wildcard address. One bound to port 0 is bound again to the wildcard address with the port it had -- [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an unconnected `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), as FreeBSD 11.4's `in_pcbconnect_setup()` and `in6_pcbconnect()` do; a connected datagram socket is disconnected first, as `soconnect()` does, and then fails with 49. 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 unless the socket is bound to a specific address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, which is the path where `udp_output()` skips `in_pcbconnect_setup()` and FreeBSD sends to port 0. There nothing can receive on port 0, so the call returns the length without the host, because Linux's `udp_sendmsg` refuses port 0 with `EINVAL`; an `AF_INET6` socket still goes to the host. The answers are taken from the FreeBSD source and are not measured on a console. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed +- [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 - [madvise](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) accepts `MADV_PROTECT` (10) as a no-op hint without modifying process protection. FreeBSD 11's `kern_madvise` (`sys/vm/vm_mmap.c`) handles it before the address check, ignores the address and length, and calls `kern_procctl(..., PROC_SPROTECT, PPROT_SET)`, which requires `PRIV_VM_MADV_PROTECT` and fails with `EPERM` without it. Returning 0 unconditionally assumes the calling process is privileged. Console behaviour is unverified - [mincore](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `sys_mincore` (`sys/vm/vm_mmap.c`), not measured on the console: the address is rounded down to the page, length 0 returns 0, a null vector over a mapped range returns `EFAULT`, an overflowing range or any unmapped page in the range (as reported by `sceKernelVirtualQuery`) returns `ENOMEM`. For valid ranges, a page reports `MINCORE_INCORE` (0x1) when `sceKernelVirtualQuery` reports it committed and 0 when it is only reserved; actual residency, referenced and modified bits are not modelled. Length 0 returns 0 when the start page is mapped and `ENOMEM` otherwise, and the range and mapping checks come before the null vector `EFAULT`, as in `kern_mincore` Java 25 LAN / DNS (PPSA99869) imports it -- [mlockall, munlockall](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mlockall` returns `EINVAL` for zero or unknown flags. Valid `MCL_CURRENT` throws because it would lock host stacks, heaps, modules and driver mappings along with guest memory; `MCL_FUTURE` throws because later mappings are not tracked for locking. 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). On main, XPSemu (PPSA97358) fails to load at these imports (mlockall #4386, 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`. A type that FreeBSD 11.4's inet domain has no protocol for in any configuration ([`in_proto.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/netinet/in_proto.c) has no `SOCK_RDM` entry and no unknown type) fails with `EPROTOTYPE` when the protocol is 0 and `EPROTONOSUPPORT` otherwise, as [`socreate`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c) does. `SOCK_RAW`, which `in_proto.c` always has (`socreate` creates it for a privileged process), and `SOCK_SEQPACKET`, which it has only in a kernel built with `SCTP`, fail with `EPROTONOSUPPORT`; whether the console allows either is not known. `SOCK_DGRAM` with `IPPROTO_UDPLITE` and `AF_UNIX` (`socketpair` supports it) fail with `EPROTONOSUPPORT` and `EAFNOSUPPORT` where FreeBSD creates the socket +- [mlockall, munlockall](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mlockall` returns `EINVAL` for zero or unknown flags. Valid `MCL_CURRENT` throws because it would lock host stacks, heaps, modules and driver mappings along with guest memory; `MCL_FUTURE` throws because later mappings are not tracked for locking. 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). 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 - [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; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating +- [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 - [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. The return values and error codes are unverified on a console - [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 -- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console +- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. `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_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL` 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`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC` and `F_SETFD` set it. None of this was measured on a console. - [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) 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 do `PROT_EXEC` and a write-only descriptor. 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`. - [File mappings on macOS](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mmap` of a regular file, of an anonymous `shm_open` object (an unlinked temporary file on macOS) or of a JIT shared memory descriptor maps the descriptor shared or private as on Linux, with the same access checks. macOS refuses to create an executable file mapping (`EPERM`), so an executable mapping is made without execute and then given it with `mprotect`. Under Rosetta 2 (Apple M2, macOS 26), code rewritten through one view of a file after it ran from another view keeps its old translation until the executable view's protection changes. Named `shm_open` objects still throw on macOS. -- [sendfile](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A socket as the file fails with `EINVAL`, as on FreeBSD, but any other descriptor `sceKernelPread` reads, such as `/dev/urandom`, is sent where FreeBSD requires a regular file or shared memory. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it +- [sendfile](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A file that is not a regular file or shared memory, such as a socket, `/dev/urandom` or a kqueue, fails with `EINVAL` and `*sbytes` 0, as FreeBSD's [`sendfile_getobj`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_sendfile.c#L421-L463) refuses it. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it - [pthread_kill](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread is left on the pending set for whichever thread waits for or unblocks it, since it is not delivered to the chosen thread, and a handle that is not a live guest thread returns `ESRCH` without touching it -- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown. -- [inet_ntop, inet_pton](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp) (libkernel) - an `AF_INET6` result of `inet_ntop` is followed by zero bytes up to `size`. In a payload process on a PS5 (firmware 12.70), the text forms, the order of the checks and this padding were measured through both exports. The padding was sampled at sizes from 3 to 65536, above 64 only at 65, 128, 256, 1024, 4096 and 65536; the sizes that were not sampled, sizes above 65536 and a title process are assumed to behave the same +- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) fails an `nfds` above 1024 with `EINVAL`, where a payload process on a PS5 (firmware 13.60) accepted 1025. Like FreeBSD 11.4's `select_check_badfd`, the console failed `EBADF` for a set bit above its last open descriptor (measured with `nfds` 1000, where AnyPS5 also fails `EBADF`); the probe's sets held 1024 bits, so its results at `nfds` 1025 and 65536 depend on the bytes past them. On a timeout with nothing ready the console cleared a set bit at `nfds` (`nfds` 16, descriptor 16), as FreeBSD writes back whole 64-bit `fd_mask` words; with `nfds` 0 the bit stayed set, as here. A timeout beyond the remaining `steady_clock` range saturates at `time_point::max()`, so the wait may never end; how long the console waits then is unknown. It accepted seconds up to `INT64_MAX` (on a ready descriptor) and failed a negative field or `tv_usec` of 1000000 or more with `EINVAL`, as here +- [inet_ntop, inet_pton](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp) (libkernel) - an `AF_INET6` result of `inet_ntop` is followed by zero bytes up to `size`. In a payload process on a PS5 (firmware 12.70), the text forms, the order of the checks and this padding were measured through both exports. The padding was sampled at sizes from 3 to 65536, above 64 only at 65, 128, 256, 1024, 4096 and 65536; the sizes that were not sampled, sizes above 65536 and a title process are assumed to behave the same. Two titles (PPSA03001 at 0x249355d, PPSA07426 at 0x186d3ca) disassembled for this entry call `inet_pton` once each with the family constant 2 and test only whether the result is 0, so 1 and -1 both take the use path; what they do with `AF_INET6` is not shown. - [_fstatfs](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports the host volume of a file, not a console mount: block sizes, block counts, name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0). The type, mount names, owner, filesystem id and I/O counters are 0 or empty. Sockets, pipes and kqueues return `EINVAL`, as FreeBSD's `getvnode` does. A character device, including `/dev/random` and `/dev/urandom`, reports FreeBSD 11.4's [`devfs_statfs`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/fs/devfs/devfs_vfsops.c#L225-L237) values on every host: no flags, 512-byte blocks, 2 blocks, nothing free and a 255-byte name limit. A Windows descriptor that is none of these throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [pthread_spin_lock](../../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) - a lock taken again by the thread that holds it throws here. On a PS5 (firmware 12.70) that call had not returned after 2 s, as FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) spins forever - [pthread_mutex_getspinloops_np, pthread_mutex_setspinloops_np, pthread_mutex_getyieldloops_np, pthread_mutex_setyieldloops_np and their scePthreadMutex counterparts](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) store and report the counts, but host mutexes do not spin or yield by them. The defaults follow FreeBSD 11.4's [`mutex_init_body`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L236-L272) without `LIBPTHREAD_SPINLOOPS` and `LIBPTHREAD_YIELDLOOPS`: 2000 spins for an adaptive mutex, 0 otherwise; the console's defaults are unverified. Setting the yield count of a destroyed mutex returns 0 as FreeBSD's [`__pthread_mutex_setyieldloops_np`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L1125-L1135) does; the console's result is unverified @@ -269,12 +285,12 @@ - [pthread_mutexattr_setkind_np](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) throws for a kind outside 1 to 4. FreeBSD 11.4's [`_pthread_mutexattr_setkind_np`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutexattr.c#L108-L120) stores any kind and [`mutex_check_attr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L207-L218) then makes `pthread_mutex_init` fail with `EINVAL`, but `pthread_mutex_init` here does not check the type - [pthread_cond_signalto_np, scePthreadCondSignalto](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) wake every waiter of the condition variable, not only the given thread, and do not check the thread; the other waiters see a spurious wakeup. Which waiters the console wakes, and its result for a thread that is not waiting, are unverified - [pthread_rwlockattr_gettype_np, scePthreadRwlockattrGettype](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) report the type stored by `scePthreadRwlockattrSettype`, which accepts any value and which read-write locks do not apply; the console's valid types and their effect are unverified. A null or uninitialized attribute throws, as for `pthread_rwlockattr_getpshared` -- [sceKernelGetEventError](../../../core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp) (libkernel) throws for `fflags` above 0xFFFF, where OR and addition of 0x80020000 differ and the console was not measured. Otherwise it returns 0 for `fflags` 0 and `0x80020000 | fflags`, whatever `flags` and `data` hold, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_queue.pas#L643) does and a PS5 (firmware 13.40, measured 2026-10-19 in a payload process) answered: `{flags 0x4000, fflags 0, data 22}`, `{0x4000, 0, -1}`, `{0x4000, 0, 0x80020016}` and `{0, 0, 0}` gave 0, `{0x4000, 5, 0}` and `{0x4000, 5, 22}` 0x80020005, `{0, 7, 22}` 0x80020007, `{0x8000, 3, 9}` 0x80020003. Video-out events carry their count in `fflags`, so they get 0x80020001 and up; only events built by the caller were measured. Closes when: `fflags` above 0xFFFF is measured - [pthread_cond_reltimedwait_np](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) takes `(cond, mutex, usec)`, a relative timeout in microseconds, and is `scePthreadCondTimedwait` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadCondTimedwait` ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/condvar.cpp#L229-L234)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `sem_reltimedwait_np`. Unverified on the console - [pthread_mutex_reltimedlock_np](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) takes `(mutex, usec)`, a relative timeout in microseconds, and is `scePthreadMutexTimedlock` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadMutexTimedlock` ([`mutex.cpp`](https://github.com/shadps4-emu/shadPS4/blob/7c960eae136b956cf4c1d72aacd2426b650330df/src/core/libraries/kernel/threads/mutex.cpp#L284-L287)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `pthread_cond_reltimedwait_np`, so a count of 2^32 microseconds or more would be truncated if the console reads 64 bits. Unverified on the console - [sysctl kern.arandom](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) serves FreeBSD 11's MIB {1, 37} (CTL_KERN, KERN_ARANDOM) and the "kern.arandom" sysctlbyname lookup through std::random_device, returning min(*oldLength, 256) random bytes. Following FreeBSD's sysctl_kern_arnd, a null buffer reports min(*oldLength, 256) without writing, a null length returns 0 without writing, and requests with a new value return EPERM (1) as the node is read-only. Cryptographic quality depends on the host's random device implementation. -- [pthread_atfork](../../../core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp) (libkernel) appends the three handlers to a process-wide list and returns 0, or `ENOMEM` when the entry cannot be allocated, as FreeBSD 11.4's [`_pthread_atfork`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_fork.c#L78-L100) does; null handlers are accepted. The handlers never run: `fork` throws and `vfork` is absent, so no child process is created. Handlers of a module that is unloaded stay in the list, where FreeBSD's `__pthread_cxa_finalize` removes them. PPSA97358 has a source-level call from the constructor `rcu_init` ([rcu.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/rcu.c#L488-L495)), but current-main runtime reachability is unverified; XPSemu currently stops first at the unresolved `mlockall` import (#4386) +- [pthread_atfork](../../../core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp) (libkernel) appends the three handlers to a process-wide list and returns 0, or `ENOMEM` when the entry cannot be allocated, as FreeBSD 11.4's [`_pthread_atfork`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_fork.c#L78-L100) does; null handlers are accepted. The handlers never run: `fork` throws and `vfork` is absent, so no child process is created. Handlers of a module that is unloaded stay in the list, where FreeBSD's `__pthread_cxa_finalize` removes them. PPSA97358 has a source-level call from the constructor `rcu_init` ([rcu.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/rcu.c#L488-L495)), but current-main runtime reachability is unverified - [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports 8192 entries on each side and counts them as a PS5 (firmware 13.60) counted them for a payload process: one cpu entry for each 2 MiB granule that holds a committed mapping, whatever its protection (none included), and one gpu entry for each 1 GiB region that holds a GPU-accessible mapping, plus one for each 2 MiB granule of such a mapping that is not a whole granule of direct memory at a 2 MiB-aligned physical address. A granule or region shared by several mappings counts once, and reservations do not count. On the console the first GPU mapping of a process also takes three gpu entries and one cpu entry that stay after it is unmapped, which is not modelled, and only the ranges whose protection is recorded here are counted. A null pointer returns `SCE_KERNEL_ERROR_EFAULT`, where the console faults. Resonance: A Plague Tale Legacy (PPSA30601) calls it at start-up (#4067) +- [settimeofday](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel_sys) checks like FreeBSD 11.4's [`kern_settimeofday`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_time.c#L688-L707) for a root caller: `EINVAL` for a negative second count or a microsecond count outside 0 to 999999, before anything is stored. A valid time throws, naming it, since the host clock is never set, and its time zone is not stored, as FreeBSD stores the zone only after setting the clock. A time zone without a time is kept for the process and reported by `gettimeofday`; `sceKernelGettimezone` still reports 0 and 0, since whether the console reads the same value is not known. FreeBSD checks `PRIV_SETTIMEOFDAY` first, which a jailed process fails with `EPERM`; whether a title holds it on the console is not known. - [clock_settime](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) validates like FreeBSD 11.4's [`kern_clock_settime`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_time.c#L396-L412) for a root caller: `EFAULT` for a null time, `EINVAL` for any clock but `CLOCK_REALTIME`, a negative second count or a nanosecond count outside 0 to 999999999. A valid `CLOCK_REALTIME` request throws, naming the requested time, since the host clock is never set and no guest clock offset is kept. FreeBSD checks `PRIV_CLOCK_SETTIME` first, which a jailed process fails with `EPERM`; whether a title holds it on the console is unverified. PS5MC (PPSA99420) imports it; no call site is known - [Short sleeps ended by GPU and flip completions](../../../core/libs/prx/libkernel/Time/SleepWake.cpp) (libkernel) - a `sceKernelUsleep`, `usleep`, `nanosleep` or `sceKernelNanosleep` of 1 ms or less returns before the requested time, which FreeBSD and the console do not do: it ends once the AGC driver reports a completed GPU batch or batch completion action, or VideoOut completes a flip, so a title that polls a GPU label or the flip status between sleeps does not sleep the rest. A title that paces frames with such a sleep can wake up to 1 ms early. A thread returns early at most 8 times in a run of sleeps; the run restarts when the thread submits AGC work or did not sleep the requested time since its last sleep, so a loop that counts sleeps to time out still sleeps all but 8 in full. `APS5_NO_SLEEP_WAKE=1` keeps every sleep in full - [chflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) follows FreeBSD 11.4's [`kern_chflagsat`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2366-L2383) and UFS's [`ufs_setattr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/ufs/ufs/ufs_vnops.c#L509-L556) for a root caller: the path lookup reports `EFAULT`, `ENOENT` and `ENOTDIR`, a flag outside the 17 UFS stores (`VNOVAL` included) `EOPNOTSUPP`, and `SF_SNAPSHOT` `EPERM`. Neither host keeps BSD file flags, so every file is taken to have none (`stat` reports `st_flags` 0): flags 0 succeed without touching the file, and any other flag throws, naming the flags. `UF_READONLY`, `UF_HIDDEN`, `UF_SYSTEM` and `UF_ARCHIVE` are FreeBSD's names for the Windows attributes but are not mapped to them: UFS stores them without enforcing them, `FILE_ATTRIBUTE_READONLY` already carries the owner write bit of `chmod` on Windows, and Linux has no equivalent a process can set without `CAP_LINUX_IMMUTABLE`. The console's filesystem types, `EROFS` on read-only mounts and the change-time update are not modelled @@ -282,7 +298,7 @@ - [getpriority](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) follows FreeBSD 11.4's [`sys_getpriority`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_resource.c#L93-L165) for the only guest process, which is taken to run at nice 0: `PRIO_PROCESS` and `PRIO_PGRP` with 0 or its own pid (the guest is taken to lead its own process group, see `kill`) and `PRIO_USER` with 0 or its user id 0 return 0, any other id fails with `ESRCH` and any other `which` with `EINVAL`. As in FreeBSD's libc, where `getpriority` is the bare system call, errno is left unchanged on success. The nice value of a title process on the console is unverified, and the host nice of the process or of the threads `APS5_THREAD_PRIORITY=high` raises is not reported. `setpriority` accepts the same target ids (any other id fails with `ESRCH`, any other `which` with `EINVAL`), clamps the value to FreeBSD's `PRIO_MIN`-`PRIO_MAX` (-20 to 20) and stores it as the guest process's nice value, which `getpriority` returns; the host scheduling class is not changed, and whether a title process may lower the value without privilege is unverified - [rtprio_thread, pthread_setconcurrency, pthread_getconcurrency](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - `RTP_SET` throws for a real-time thread (sce attributes allow only `SCHED_FIFO`/`SCHED_RR`) and for another thread: a payload process on a PS5 (firmware 12.70) answered type 2, prio 0/15/31 on the calling thread with `EINVAL`; no accepted value or set by another thread's id measured. A non-real-time thread (POSIX attributes store `SCHED_OTHER`), unmeasured, reports 3 with 0, and `RTP_SET` accepts only that. `RTP_LOOKUP` reports the policy's type (10 or 2) and the scheduling priority, as measured, not FreeBSD's 0-31; another function fails with `ESRCH` (2 measured), a null `rtprio` with `EFAULT`, an id of no live thread with `ESRCH` (unknown ids measured). `pthread_setconcurrency` stores 0 or more, returns `EINVAL` (not errno) below 0, as libthr does; `pthread_getconcurrency` returns it, a hint. Morrowind (`mshivam019/morrowind-ps5`) imports them - [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) returns [VirtualQueryInfo](../../../core/libs/SceTypes.hpp) with a one-byte flag allocation unit so Windows and SysV hosts return the same 72-byte representation, with the name at byte 33 and GPU mask at byte 65. This preserves the existing SysV representation; the console offsets beyond the address, protection and size fields observed in Astro Bot's AGC calls are not hardware-measured. Size and offset assertions plus a raw-byte named-mapping regression guard the host ABI. -- [app0.zar files](../../../core/libs/prx/libkernel/File/src/ArchivedFile.cpp) (libkernel) - `sceKernelOpen`, `open`, `sceKernelStat` and `sceKernelCheckReachability` resolve a path missing from `app0/` in `app0.zar`. An archived file is a read-only host null device whose reads, positioned and vectored reads, AIO reads, seeks, `fstat` and `FIONREAD` are served from the archive, with the position shared by `dup`, `dup2` and `F_DUPFD`; writes fail with `EBADF` as on a read-only descriptor, opening one for writing or truncation fails with `EROFS` and `O_CREAT | O_EXCL` with `EEXIST`. `fstat` and `sceKernelStat` report mode `0444` (`0555` for a directory), the node index as inode and the archive file's device and times. Opening an archived directory or a directory present both on disk and in the archive throws, as `getdents` would miss entries. On an archived descriptor `mmap`, `flock`, record locks, `fstatfs`, `ftruncate`, `fsync`, `fdatasync`, `getdirentries`, `fchmod` and `futimes` throw instead of acting on the null device; `mmap` is `noexcept`, so its refusal terminates the process. `lstat` and the other path calls on archived files are not implemented +- [app0.zar files and directories](../../../core/libs/prx/libkernel/File/src/ArchivedFile.cpp) (libkernel) - archive directory snapshots refresh on rewind; ordering and seek cookies are host packaging behavior, not console-measured behavior. Disk entries shadow archive names under the archive component comparison. Virtual directory descriptors reject data I/O, mapping, locks, filesystem queries and metadata mutation before touching their null-device backing; ordinary host directories retain native operations. Archived files remain read-only, with duplicate descriptors sharing their cursor. `lstat`, access and other ordinary path operations on archive-only entries remain unsupported. Validation uses synthetic archives on Windows and WSL, without PS5 or title runs. - [sceKernelLoadStartModule](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) loads a system library named by its console path, `/system/common/lib/.sprx` or the sandboxed `//common/lib/.sprx` that `sceKernelGetFsSandboxRandomWord` names, from the host `libs/.prx`, as libSceSysmodule loads modules by ID; a host library has no `module_start`, so `res` is 0. Other system directories, such as `/system/priv/lib`, are not mapped. The OpenOrbis piglet sample loads `libScePigletv2VSH.sprx` and `libScePrecompiledShaders.sprx` this way - [param.sfo](../../../core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp) (libkernel) is read when `param.json` is absent, for `TITLE`, `TITLE_ID`, `DOWNLOAD_DATA_SIZE` and `USER_DEFINED_PARAM_1` to `_4`, and mounts `/data` and `/download0` like `param.json`; the localized `TITLE_00` to `TITLE_30` are not selected by the console language. - [sysctl MIBs for kern.smp.maxcpus, kern.stackprot, kern.sched.name and kern.timecounter.hardware](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - on a PS5 (firmware 12.70), a payload process resolves these names to {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}; `sysctl` returns -1 with `ENOENT` for reads and size queries by those exact MIBs, leaving the buffer and length unchanged, including when either or both output pointers are null. Writes were not measured and throw. `sysctlnametomib` still throws for these four names; #7541 implements the mappings for nine of the other names. diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 5f015be839..6987cb2840 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -15,21 +15,23 @@ - [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 - [lchmod](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws for a symbolic link on Windows and Linux, whose hosts cannot change a link's own mode; on macOS the link's mode is changed. Anything else goes to `fchmodat(AT_FDCWD, path, mode, AT_SYMLINK_NOFOLLOW)`, as FreeBSD 11.4's [`sys_lchmod`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2490-L2496) does, so a file or directory is changed as by `chmod`, with `fchmodat`'s path lookup and Windows mode mapping. On Windows any reparse point counts as a link. Console behaviour is unverified -- [pathconf, fpathconf, _fpathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) report FreeBSD 11's `ufs_pathconf` constants for files and directories alike (`_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0); whether the console's filesystems report the same is not verified. They throw for names whose UFS value depends on the mount, the kernel options or the page size, and where UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, and `_PC_ASYNC_IO` in `pathconf`); other names fail with `EINVAL`. `fpathconf` and `_fpathconf` answer `_PC_ASYNC_IO` 200112 and, for a pipe, `_PC_PIPE_BUF` 512 and `EINVAL` otherwise, as FreeBSD 11's `kern_fpathconf` does; a guest socket is not a host descriptor, so the `fstat` check reports it as `EBADF`, as in `fchown` +- [pathconf, fpathconf, _fpathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) answer every file and directory as the console's UFS mount `/system_data` does in a payload process on a PS5 (firmware 13.60). Other mounts differ: `/data` and `/user` give `_PC_LINK_MAX` 1, `_PC_SYMLINK_MAX` 0, `_PC_NO_TRUNC` `EINVAL` and 255 for `_PC_MAX_CANON`, `_PC_MAX_INPUT` and `_PC_VDISABLE`; `/`, `/system`, `/hostapp` and `/update` answer only four names; tmpfs gives `_PC_FILESIZEBITS` 0 and `_PC_SYNC_IO` 1. What a title sees under `/app0`, `/savedata0`, `/temp0` and `/download0` is not known. `fpathconf` and `_fpathconf` answer `_PC_ASYNC_IO` 200112 and, for a pipe, only `_PC_PIPE_BUF` 512, as FreeBSD 11's `kern_fpathconf` does; a guest socket fails `fstat` with `EBADF` - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified - [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction returns the same kind of name when it is a drive path (`X:\...`), using the print name when that is a drive path and the substitute name otherwise. A mounted volume (`\??\Volume{GUID}\`), any other name surrogate and every reparse point without the name surrogate bit (WOF compression, deduplication, cloud files) return `EINVAL`, as FreeBSD does for a file that is not a link and as CPython's `os.readlink`, which reads only symbolic links and junctions, does - [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` - [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 +- [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 - [fstatat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `stat`, or to `lstat` with `AT_SYMLINK_NOFOLLOW` (0x200). Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory, and flags other than `AT_SYMLINK_NOFOLLOW` return `EINVAL`. PPSA74126 imports it. Console behaviour is unverified - [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified - [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified - [fork](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask -- [sceKernelGetModuleInfoInternal and sceKernelGetModuleListInternal](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) answer for every loaded module, system modules included. `sceKernelGetModuleInfoInternal` fills the 0x1A8-byte `SceKernelModuleInfoEx` of a module handle the way `sceKernelGetModuleInfoFromAddr` does for an address, with [shadPS4's checks](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/kernel/process.cpp#L243-L258) in their order: `EFAULT` for a null pointer, `EINVAL` when `st_size` is not 0x1A8, `ESRCH` for an unknown handle. `sceKernelGetModuleListInternal` returns the same list as `sceKernelGetModuleList`, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_libkernel.pas#L852-L858) does; fpPS4 marks that as a guess, and neither is checked on a PS5 +- [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` - [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 capability bits remain unverified on PS5; they follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/kernel/kernel.cpp), leaving the authority ID and other capability, attribute and credential words zero. The host models a retail unit (`IsCEX` 1, `IsDevKit` 0). The system OpenPSID reuses the ordinary ID; firmware 13.42 reads a separate `machdep.openpsid_for_sys`, whose value is unknown. Null returns positive 22 per the [module report](https://github.com/boykopovar/AnyPS5/pull/10524#issuecomment-6393566914). - [fchown](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled: the guest's own owner and group (0) and the unchanged value (-1) are accepted, a descriptor that is not open fails with `EBADF`, and any other owner or group throws, naming it. The check reads the descriptor with `fstat`, ignoring the CRT's invalid-parameter handler on Windows as `DescriptorControl` does. The console's ownership rules are unverified. Morrowind (`mshivam019/morrowind-ps5`) imports it - [chown, lchown and fchownat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled, as for `fchown`: owner and group 0 and the unchanged value (-1) are accepted, and any other owner or group throws, naming it. The path is looked up first, as in FreeBSD 11's `kern_fchownat`, with `stat`, with `lstat` for `lchown`, and with `fstatat` for `fchownat`, so a lookup error is returned before the owner is checked and nothing is changed on the host. `fchownat` flags other than `AT_SYMLINK_NOFOLLOW` (0x200) return `EINVAL`, as in FreeBSD 11. The console's ownership rules are unverified -- [linkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `link`, and flags other than `AT_SYMLINK_FOLLOW` (0x400) return `EINVAL`, as in FreeBSD 11. A relative path with a directory descriptor throws, and so does a symbolic link as the source without `AT_SYMLINK_FOLLOW`, where FreeBSD links the symbolic link itself. Console behaviour is unverified +- [linkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) resolves `AT_FDCWD` (-100) and absolute paths as `link` does, and flags other than `AT_SYMLINK_FOLLOW` (0x400) return `EINVAL`, as in FreeBSD 11. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. A symbolic link as the source without `AT_SYMLINK_FOLLOW` throws, where FreeBSD links the symbolic link itself. Console behaviour is unverified - [pthread_set_defaultstacksize_np and scePthreadSetDefaultstacksize](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) set the stack size that `scePthreadAttrInit` and a thread created without attributes use, as [fpPS4's `pthread_set_defaultstacksize_np`](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_pthread_attr.pas#L89-L102) does: a size of 16384 bytes or less fails with `EINVAL` (`0x80020016` from the sce form) and there is no upper limit, so a default the host cannot allocate fails only when a thread is created. Attributes initialised earlier keep their size. Not checked on a PS5 - [sceKernelTitleWorkaroundIsEnabled](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - not measured on a console; the `{version, align, ids[2]}` layout, the bit test and the limit of 0x3A bits follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/kernel/kernel.cpp), which notes that the limit changes between firmwares. A negative bit is `EINVAL` here; shadPS4 compares it signed and reads outside `ids`. Titles usually pass the `title_workaround` block of `sceKernelGetAppInfo`, which does not write that block here, so the bits read are whatever the caller's memory held - [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` diff --git a/docs/dev/debt/libmdbg_syscore.md b/docs/dev/debt/libmdbg_syscore.md index cb843c4f05..0201062426 100644 --- a/docs/dev/debt/libmdbg_syscore.md +++ b/docs/dev/debt/libmdbg_syscore.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceDebugAcquireAndUpdateDebugRegister, sceDebugAttachProcess, sceDebugCancelCoredump, sceDebugClearStepThread, sceDebugCreatePerfScratchDataArea, sceDebugCreatePerfScratchExecutableArea, sceDebugCreateScratchDataArea, sceDebugCreateScratchDataAreaForPrx, sceDebugCreateScratchExecutableArea, sceDebugCreateScratchExecutableAreaForPrx, sceDebugDestroyPerfScratchDataArea, sceDebugDestroyPerfScratchExecutableArea, sceDebugDestroyScratchDataArea, sceDebugDestroyScratchExecutableArea, sceDebugDetachProcess, sceDebugGetApplicationIdByTitleId, sceDebugGetApplicationInfo, sceDebugGetApplicationList, sceDebugGetCrashInfoDetailForCoredump, sceDebugGetCrashInfoForCoredump, sceDebugGetDLLoadFlag, sceDebugGetDataListOfUltQueue, sceDebugGetDebugRegisterStatusMap, sceDebugGetEventList, sceDebugGetEventListForEQueueFd, sceDebugGetEventSubscriptionList, sceDebugGetEventSubscriptionListForEQueueFd, sceDebugGetFiberInfo, sceDebugGetFileInfo, sceDebugGetFileInfoForCoredump, sceDebugGetFileList, sceDebugGetFileListForCoredump, sceDebugGetGpuInfoArea, sceDebugGetJobManagerInfo, sceDebugGetJobManagerSequenceInfo, sceDebugGetJobManagerSequenceList, sceDebugGetModuleInfo, sceDebugGetModuleList, sceDebugGetModuleMetaData, sceDebugGetMonoVMInfo, sceDebugGetMonoVMList, sceDebugGetProcessCoredumpHandlerEventBlob, sceDebugGetProcessCoredumpHandlerEventInfo, sceDebugGetProcessCoredumpHandlerResult, sceDebugGetProcessEventCntlFlag, sceDebugGetProcessInfo, sceDebugGetProcessList, sceDebugGetProcessPropertyForCoredump, sceDebugGetProcessResourceStatCount, sceDebugGetProcessResourceStatData, sceDebugGetProcessTime, sceDebugGetProcessTimeCounter, sceDebugGetSyncExclusiveWaiterList, sceDebugGetSyncObjectData, sceDebugGetSyncObjectList, sceDebugGetSyncWaiterList, sceDebugGetSyncWaiterListForEQueueFd, sceDebugGetSystemStatusBlob, sceDebugGetSystemStatusCount, sceDebugGetThreadInfo, sceDebugGetThreadInfoByIdent, sceDebugGetThreadList, sceDebugGetThreadListAsIdent, sceDebugGetUlObjectList, sceDebugGetUltCondvarInfo, sceDebugGetUltInfo, sceDebugGetUltListOfUltRuntime, sceDebugGetUltMutexInfo, sceDebugGetUltQueueDataResourcePoolInfo, sceDebugGetUltQueueInfo, sceDebugGetUltRuntimeInfo, sceDebugGetUltRwlockInfo, sceDebugGetUltSemaphoreInfo, sceDebugGetUltWaitingQueueResourcePoolInfo, sceDebugGetVirtualMemoryDetailInfoForCoredump, sceDebugGetVirtualMemoryInfo, sceDebugGetVirtualMemoryInfoForCoredump, sceDebugGetWaitingListOfUltCondvar, sceDebugGetWaitingListOfUltMutex, sceDebugGetWaitingListOfUltQueue, sceDebugGetWaitingListOfUltRwlock, sceDebugGetWaitingListOfUltSemaphore, sceDebugGetWorkerThreadListOfUltRuntime, sceDebugInit, sceDebugInitForCoredump, sceDebugInitForTest, sceDebugIpmiGetBlockTimeInfoList, sceDebugIpmiGetBlockedIpcInfo, sceDebugIpmiGetChannelInfo, sceDebugIpmiGetChannelKidList, sceDebugIpmiGetChannelWaitingThreadList, sceDebugIpmiGetClientInfo, sceDebugIpmiGetClientKidList, sceDebugIpmiGetClientKidListByDump, sceDebugIpmiGetClientKidListByServerKid, sceDebugIpmiGetConnectRequestInfoList, sceDebugIpmiGetConnectionInfoList, sceDebugIpmiGetConnectionWaitingThreadListByClientKid, sceDebugIpmiGetConnectionWaitingThreadListBySessionKid, sceDebugIpmiGetDump, sceDebugIpmiGetEncryptedInfoAllForCoredump, sceDebugIpmiGetKidInfoListForCoredump, sceDebugIpmiGetServerDispatchInfo, sceDebugIpmiGetServerInfo, sceDebugIpmiGetServerKidList, sceDebugIpmiGetServerKidListByDump, sceDebugIpmiGetServerWaitingThreadList, sceDebugIpmiGetSessionInfo, sceDebugIpmiGetSessionKidList, sceDebugIpmiGetSessionKidListByServerKid, sceDebugIpmiGetTidListByDump, sceDebugKillApplication, sceDebugKillProcess, sceDebugNoStopChildProcesses, sceDebugNoStopOnDLLoad, sceDebugProcessSpawn, sceDebugReadEvent, sceDebugReadEventForTest, sceDebugReadProcessMemory, sceDebugReadProcessMemoryForSDBGP, sceDebugReadProcessRegister, sceDebugReadProcessRegisterForSDBGP, sceDebugReadProcessResourceStatData, sceDebugReadThreadRegister, sceDebugReadThreadRegisterForSDBGP, sceDebugReleaseDebugRegister, sceDebugResumeApplication, sceDebugResumeProcess, sceDebugResumeThread, sceDebugSetProcessEventCntlFlag, sceDebugSetStepThread, sceDebugSpawnApplication, sceDebugStopChildProcesses, sceDebugStopOnDLLoad, sceDebugSuspendApplication, sceDebugSuspendProcess, sceDebugSuspendThread, sceDebugTriggerCoredump, sceDebugTriggerCoredumpForSystem, sceDebugTriggerVrCaptureDump, sceDebugWriteProcessMemory, sceDebugWriteProcessRegister and sceDebugWriteThreadRegister](../../../core/libs/prx/libmdbg_syscore/Export.cpp) (libmdbg_syscore) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libmdbg_syscore.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 +- [sceDebugAcquireAndUpdateDebugRegister, sceDebugAttachProcess, sceDebugCancelCoredump, sceDebugClearStepThread, sceDebugCreatePerfScratchDataArea, sceDebugCreatePerfScratchExecutableArea, sceDebugCreateScratchDataArea, sceDebugCreateScratchDataAreaForPrx, sceDebugCreateScratchExecutableArea, sceDebugCreateScratchExecutableAreaForPrx, sceDebugDestroyPerfScratchDataArea, sceDebugDestroyPerfScratchExecutableArea, sceDebugDestroyScratchDataArea, sceDebugDestroyScratchExecutableArea, sceDebugDetachProcess, sceDebugGetApplicationIdByTitleId, sceDebugGetApplicationInfo, sceDebugGetApplicationList, sceDebugGetCrashInfoDetailForCoredump, sceDebugGetCrashInfoForCoredump, sceDebugGetDLLoadFlag, sceDebugGetDataListOfUltQueue, sceDebugGetDebugRegisterStatusMap, sceDebugGetEventList, sceDebugGetEventListForEQueueFd, sceDebugGetEventSubscriptionList, sceDebugGetEventSubscriptionListForEQueueFd, sceDebugGetFiberInfo, sceDebugGetFileInfo, sceDebugGetFileInfoForCoredump, sceDebugGetFileList, sceDebugGetFileListForCoredump, sceDebugGetGpuInfoArea, sceDebugGetJobManagerInfo, sceDebugGetJobManagerSequenceInfo, sceDebugGetJobManagerSequenceList, sceDebugGetModuleInfo, sceDebugGetModuleList, sceDebugGetModuleMetaData, sceDebugGetMonoVMInfo, sceDebugGetMonoVMList, sceDebugGetProcessCoredumpHandlerEventBlob, sceDebugGetProcessCoredumpHandlerEventInfo, sceDebugGetProcessCoredumpHandlerResult, sceDebugGetProcessInfo, sceDebugGetProcessList, sceDebugGetProcessPropertyForCoredump, sceDebugGetProcessResourceStatCount, sceDebugGetProcessResourceStatData, sceDebugGetProcessTime, sceDebugGetProcessTimeCounter, sceDebugGetSyncExclusiveWaiterList, sceDebugGetSyncObjectData, sceDebugGetSyncObjectList, sceDebugGetSyncWaiterList, sceDebugGetSyncWaiterListForEQueueFd, sceDebugGetSystemStatusBlob, sceDebugGetSystemStatusCount, sceDebugGetThreadInfo, sceDebugGetThreadInfoByIdent, sceDebugGetThreadList, sceDebugGetThreadListAsIdent, sceDebugGetUlObjectList, sceDebugGetUltCondvarInfo, sceDebugGetUltInfo, sceDebugGetUltListOfUltRuntime, sceDebugGetUltMutexInfo, sceDebugGetUltQueueDataResourcePoolInfo, sceDebugGetUltQueueInfo, sceDebugGetUltRuntimeInfo, sceDebugGetUltRwlockInfo, sceDebugGetUltSemaphoreInfo, sceDebugGetUltWaitingQueueResourcePoolInfo, sceDebugGetVirtualMemoryDetailInfoForCoredump, sceDebugGetVirtualMemoryInfo, sceDebugGetVirtualMemoryInfoForCoredump, sceDebugGetWaitingListOfUltCondvar, sceDebugGetWaitingListOfUltMutex, sceDebugGetWaitingListOfUltQueue, sceDebugGetWaitingListOfUltRwlock, sceDebugGetWaitingListOfUltSemaphore, sceDebugGetWorkerThreadListOfUltRuntime, sceDebugInit, sceDebugInitForCoredump, sceDebugInitForTest, sceDebugIpmiGetBlockTimeInfoList, sceDebugIpmiGetBlockedIpcInfo, sceDebugIpmiGetChannelInfo, sceDebugIpmiGetChannelKidList, sceDebugIpmiGetChannelWaitingThreadList, sceDebugIpmiGetClientInfo, sceDebugIpmiGetClientKidList, sceDebugIpmiGetClientKidListByDump, sceDebugIpmiGetClientKidListByServerKid, sceDebugIpmiGetConnectRequestInfoList, sceDebugIpmiGetConnectionInfoList, sceDebugIpmiGetConnectionWaitingThreadListByClientKid, sceDebugIpmiGetConnectionWaitingThreadListBySessionKid, sceDebugIpmiGetDump, sceDebugIpmiGetEncryptedInfoAllForCoredump, sceDebugIpmiGetKidInfoListForCoredump, sceDebugIpmiGetServerDispatchInfo, sceDebugIpmiGetServerInfo, sceDebugIpmiGetServerKidList, sceDebugIpmiGetServerKidListByDump, sceDebugIpmiGetServerWaitingThreadList, sceDebugIpmiGetSessionInfo, sceDebugIpmiGetSessionKidList, sceDebugIpmiGetSessionKidListByServerKid, sceDebugIpmiGetTidListByDump, sceDebugKillApplication, sceDebugKillProcess, sceDebugNoStopChildProcesses, sceDebugNoStopOnDLLoad, sceDebugProcessSpawn, sceDebugReadEvent, sceDebugReadEventForTest, sceDebugReadProcessMemory, sceDebugReadProcessMemoryForSDBGP, sceDebugReadProcessRegister, sceDebugReadProcessRegisterForSDBGP, sceDebugReadProcessResourceStatData, sceDebugReadThreadRegister, sceDebugReadThreadRegisterForSDBGP, sceDebugReleaseDebugRegister, sceDebugResumeApplication, sceDebugResumeProcess, sceDebugResumeThread, sceDebugSetStepThread, sceDebugSpawnApplication, sceDebugStopChildProcesses, sceDebugStopOnDLLoad, sceDebugSuspendApplication, sceDebugSuspendProcess, sceDebugSuspendThread, sceDebugTriggerCoredump, sceDebugTriggerCoredumpForSystem, sceDebugTriggerVrCaptureDump, sceDebugWriteProcessMemory, sceDebugWriteProcessRegister and sceDebugWriteThreadRegister](../../../core/libs/prx/libmdbg_syscore/Export.cpp) (libmdbg_syscore) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libmdbg_syscore.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/relinker.md b/docs/dev/debt/relinker.md index 241d10f16f..f2d970f219 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -10,7 +10,8 @@ - [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. - [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. -- Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) by default, not only with `--windows-fixed-base`: on the console the executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Relocations are kept, so if `0x140000000` is taken Windows relocates the image, possibly above that window. Verified only on Windows 10. +- 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. +- 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 instruction sequences from [ps5-payload-dev SDK libc/mman.c](https://github.com/ps5-payload-dev/sdk/blob/12cf3dda40/libc/mman.c), observed in Nuvio Maxd 0.5.15 (PPSA99288): syscall 74 and the two syscall 477 paths, with their argument preparation and carry-flag error handling. Matching requires a decoded instruction boundary and the complete sequence inside the scanned code range. Other SDK/compiler output remains rejected. The matched syscall becomes `UD2`; this does not implement mmap or mprotect and a call still faults. - The [instruction decoder](../../../core/relinker/codegen/src/x86/X64InstructionDecoder.cpp) reads a 16-bit displacement for `66 E8`, `66 E9` and `66 0F 80`-`8F` without REX.W, as AMD processors, the console included, do (binutils `objdump -M amd64`; REX.W selects the 32-bit form). AMD also truncates the target of every near relative branch with that prefix, rel8, LOOP and JRCXZ forms included, to 16 bits. Instruction metadata marks these branches as truncated and reports no branch target for them, and stubs refuse to move them. Compilers do not emit these forms. - Converted Linux main executables are [placed at `0x400000`](../../../core/relinker/relinker/src/pipeline/Conversion.cpp) by default, as `ET_EXEC` without ASLR, not only with `--linux-fixed-base`: the console executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Where the console loads an executable that does not require an address is not measured; `0x400000` is the base that executables reading their GOT through `app0/imports.txt` assume (#4013). The relinker refuses an image that spans 2 GiB or more, so the executable ends below the guest arena at `0x200000000`. With `--lazy-binding` the executable stays position independent. @@ -30,6 +31,7 @@ - In the same executables and modules, the bytes between unwind functions are 16-byte slots of function tails and padding, and a tail can begin inside an instruction. The [instruction scanner](../../../core/relinker/codegen/src/InstructionScanner.cpp) of the syscall and call-site scans therefore resumes at the next byte when bytes outside every unwind function do not decode, and ends a decoding outside them at the start of the next unwind function. Code without an unwind entry that does not decode from its first byte is skipped by these scans instead of failing the relink; inside an unwind function a decode failure still fails it. - Read-only data can also sit between the unwind functions of such images. There, a `syscall`, `sysenter` or `int 0x80` pattern fails the [syscall scan](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) only when a constant syscall number is set before it; the others, and every `sysret`, are skipped and counted in one message. The number is looked for in the decoded instructions and, whatever alignment the decoding took, in the bytes before the pattern: a `mov eax, imm32`, `mov rax, imm32`, `mov rax, imm64` or `xor eax, eax`, optionally followed by `mov al, imm8` or `mov ax, imm16`, then by any number of register moves or zeroings that leave `rax` alone. A real syscall without an unwind entry whose number is set otherwise is not refused and runs on the host: from memory or another register (`mov eax, ecx`), by `push`/`pop` or arithmetic (`or eax, imm32`), or with any other instruction between the load and the pattern. - In the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs for such images, a relocation or `lea` target outside every unwind function is followed as code only when its linear decoding, up to the first jump, return or trap, the next unwind function or 256 instructions, holds an `fs:[disp32]` access, or when it is 16-byte aligned, starts less than 256 bytes after an unwind function and ends in a jump, return or trap. Every other such target is taken as data and counted in one message, and the Windows TLS rewrite fails instead of overwriting one. A TLS access of another form, such as `mov rax, fs:[rax]`, or a `gs` instruction in code reached only through such a target is therefore not rewritten or trapped. Other fs and gs instructions do not tell code from data there: string bytes decode as these prefixes, and in two measured titles about 2,500 of about 5,500 such targets decode to one before their first jump. +- [Embedded data tables and relative jump tables in inter-FDE gaps](../../../core/relinker/relinker/src/analysis/RelativeJumpTables.cpp) - in modules with `PT_GNU_EH_FRAME` or `PT_SCE_DYNLIBDATA`, a constant table of undecodable bytes that a RIP-relative `LEA` references, and a trailing relative jump table, are masked with `0x90` in the scan buffer before syscall scanning and kept out of the code roots of [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp), so `0F 78` data in `ubiservices.prx` (PPSA28155, #7547) is no longer refused as an illegal opcode. Masking starts at the `LEA` target, stops at the first byte whose linear decoding reaches a jump, return or trap, keeps gap code being scanned, and needs padding only from the function end to the table; A gap that decodes as valid instructions or that a direct branch targets is left alone. The gap properties are computed once in a backward pass. Output bytes are unchanged apart from syscall traps. - In executables and guest modules with a `PT_SCE_DYNLIBDATA` segment, the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs follows code only from the end of `PT_INTERP`, or of the `/libexec/ld-elf.so.1` string that begins the segment when no `PT_INTERP` covers it, as in OpenOrbis modules, to the end of the PLT after the last function of the unwind table, or to the end of that function when the PLT comes before the code, as in the `libSceFios2.prx` and `libc.prx` that CUSA02636 bundles (PLT at 0xc8); the pipeline's scans of executables above stop at this same end, also when the PLT comes before the code. A guest module with PLT relocations and no PLT before or after its unwind functions, or with a PLT that overlaps one, fails as the executable does. Code after that end is not analyzed, such as the `DT_FINI` routine without an unwind entry that follows the last unwind function in both modules and that the Windows and macOS outputs call when the program exits: a TLS access in it is not rewritten, the ELF entry point, `DT_INIT`, `DT_FINI`, a branch or a pointer into it is ignored, and a function symbol or init/fini array entry pointing into it fails the conversion. - Under Rosetta (M5, macOS 27.0.1), a VMASKMOVPS, VMASKMOVPD, VPMASKMOVD or VPMASKMOVQ store or load that faults on an element other than an active first one ends the process (`unexpectedly need to EmulateForward on a synchronous exception`) instead of raising SIGSEGV or SIGBUS, so a masked store into a page the macOS write watch keeps read-only ends the title. `--to-rosetta` moves each masked store to a [stub](../../../core/relinker/codegen/src/x86/MaskedStoreLowering.cpp) that first writes to the first and last byte of every active element with `LOCK OR` of 0, which faults normally and changes nothing; each store then costs up to 16 locked byte writes. A page made read-only again by another thread between those writes and the store still ends the process. A stack-relative displacement that exceeds signed disp32 after the spill frame throws. Masked loads are not converted. - 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)). @@ -39,6 +41,6 @@ - `--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, 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 reports the same CPUID but executes 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. 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)) exists for Windows and Linux output, not macOS. Only imports bound through a PLT slot, or through a GOT slot whose symbol is a function, are traced; a function reached through any other relocation, or looked up at run time with `sceKernelDlsym`, is not, and neither are weak imports or the imports of modules 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 +- `--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 - [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; other BMI instructions are not lowered. 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. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 6919cefeed..e7aba24b21 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -22,7 +22,7 @@ - [s_subvector_loop_begin and s_subvector_loop_end](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. The begin instruction tests for an empty mask as `(exec_lo | exec_hi) == 0`: AMD's Windows driver (32.0.21045.5002 and 32.0.21042.62) compiled `exec_lo == 0 && exec_hi == 0` as a test of the whole 64-bit mask when guest loads use selects, as they do with `robustBufferAccess2` (#7376, #7623). Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge, which SPIR-V requires exactly once per loop header (SPIRV-Tools rejects the module and the NVIDIA driver 615.78.08 crashes compiling it); a loop whose back-edge block becomes unreachable in another way is not handled - [Vertex entry merged wave info](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) - `s3` of every vertex-path shader is set to `waveSize | waveSize << 8` (vertex and primitive count of a full wave, wave index 0), or to `32 | 32 << 8` for a wave64 program on a 32-lane host subgroup, which runs it at one lane per invocation and holds no lanes 32-63. On a device whose `subgroupSupportedStages` lacks the vertex stage (lavapipe, MoltenVK), vertex and local programs run as one-lane waves: `s3` is `1 | 1 << 8`, the lane id is 0, a ballot is the invocation's own bit, a shuffle or broadcast returns the invocation's value, an elect is true and a subgroup reduction is the value itself, so no `GroupNonUniform` capability is declared. Layout taken from the prologue Mesa's ACO emits for NGG vertex shaders (`s_bfe_u64 exec` from `s3`), not measured on hardware. The driver gives every graphics program user data from `s8`, so the condition does not tell NGG from legacy vertex shaders: they just never read `s3`. A draw whose last wave is partial still reports a full wave, and so does a host wave with idle invocations (an RTX 5070 ran every vertex warp of a non-indexed triangle list with 30 of its 32 invocations). - [s_barrier in vertex-path programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another, or when the barrier runs before every memory access of the program (it is on the entry's straight-line path with no load, store, atomic, image or LDS access ahead of it), so it orders nothing; the `GS_ALLOC_REQ` such a barrier orders on NGG passthrough programs is already dropped on the vertex path. The Vertex execution model has no workgroup barrier. Other programs with LDS, GDS or memory writes throw. Not measured on hardware -- [Vertex clip and cull distances](../../../core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp) (libSceAgcDriver) - `PA_CL_VS_OUT_CNTL` enable bit `s` of `CLIP_DIST_ENA`/`CULL_DIST_ENA` is taken to select component `s` of the `pos1`/`pos2` exports, with the Vulkan array index counted by the enabled bits below it, and `VS_OUT_CCDIST0_VEC_ENA` as bit 22; this follows the recompiler's existing position-export mapping, not hardware or documentation. The guest clip distances share one `gl_ClipDistance` array with the all-zero clip position distance, which takes the index after them. A vertex program whose own code has no `pos1`..`pos3` export is compiled and prepared without these bits (`PositionExportLayout`), so registration and draws agree on its artifact; an export in a subroutine it calls is not seen by that scan +- [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 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. @@ -54,7 +54,7 @@ - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. - Direct [FMASK image loads](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) of 16 samples, CMASK compression (bar the 8-fragment native owner path), packed accesses and other layouts throw. Uncompressed FMASK of up to 8 samples reads a single-mip 2D or 2D-array 8-, 16- or 32-bit `UINT` snapshot with `Z_64KB_X` addressing; selectors may name X, zero or one. `Fmask32_S8_F8` returns the stored word; others expand to 4 bits per sample, an index at or past the fragment count reading 8 (unknown), samples past the count 0. Layouts follow PAL's [expanded values](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9MaskRam.cpp#L5005) and [FMASK surfaces](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L877). Console results, unused nibbles included, are unmeasured; compressed color sampling is unsupported. - A [DIM 2D read of a 2D MSAA T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) returns sample 0, which is not measured on the console's gfx1013, at an implicit LOD above 0, at an explicit one for texels over 16 bits except formats 22 and 56 in SW_64KB_R_X, or for other formats and swizzle modes; 64- and 128-bit SW_64KB_Z_X texels are not covered: gfx1031 does not address their samples. Such a read is an `image_load` without a mip, `image_sample`, `image_sample_lz` or `image_sample_l` of a direct sampled image; other accesses (gathers, bias, gradients, offsets, LOD clamps, depth compares, `image_load_mip`, `image_get_resinfo`, `image_get_lod`, PCK, BY2/BY4) throw, as does a depth-bits T# (16_UINT, 32_UINT or 32_SINT in a Z swizzle mode). On gfx1031 (RX 6700 XT, HIP), such reads return sample 0 at 2x, 4x and 8x for 8- to 128-bit texels in SW_64KB_R_X and 8- to 32-bit ones in SW_64KB_Z_X (formats 1, 5, 7, 11, 13, 14, 18, 22, 29, 56, 60, 64, 71, 77, 133). -- `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 image atomics through a table lower to `OpAtomic*` on the table `AtomicView` (`Device` scope, relaxed, `AcquireRelease`/`UniformMemory` barrier, as the direct path) instead of faulting; 64-bit atomics still fault (no `Atomic64View` on the table path). +- `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. - 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. @@ -67,7 +67,7 @@ - [MIMG `tfe` and `lwe`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp) never write the status VGPR after the data. On hardware it is written only when a texel fetch hits an unmapped page of a partially resident texture, or, for `lwe` on sample/gather, when the LOD is below the T# `MIN_LOD_WARN`; texture memory is always resident here, the LOD warning (texture-streaming feedback, like the T# `MIN_LOD_WARN` itself) is never reported, and `lwe` on `image_get_lod` throws. - [Packed horizontal gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_gather4h_pck`, MIMG opcode 98): the four window texels are read raw and concatenated into a little-endian bitstream, with DMASK bit i writing stream word i to the next destination register; measured on RDNA2 (gfx1031) at every edge: the anchor is floor(x-0.5) with elements at -1, 0, +1 and +2 clamped horizontally into the width, the row is floor(y), negative rows clamp to row 0, rows at or past the height return zeros, sampler addressing modes do not affect the window, and formats wider than 32 bits per texel write zeros in every register. The emitter fetches each window texel at LOD 0 and packs its bits like `image_load_pck`, so the sampler's filter, addressing and LOD state are not used; formats whose bits are not recoverable from the view and D16 data throw. The anchor and row multiply the normalized coordinate by the image size without contraction. The stream rule was measured for R8_UNORM, RG8_UNORM and RGBA8; only RGBA8 has an execution test. `image_gather8h_pck` (opcode 99) never returns texel data on this hardware - across dmasks 0x1 to 0xFF including the upper nibble, RGBA8, RGBA16 and RGBA32 formats, 1D, 2D and 3D descriptors and normalized and unnormalized sampling it only zero-fills popcount(dmask) registers (sentinel-prefilled destinations) - so it decodes but throws at translation; its eight-texel semantics remain unmeasured. - [MIMG BY2/BY4 loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_load[_mip]_by2/by4`, opcodes 0x42, 0x43, 0x4a and 0x4b) were measured on RDNA2 (gfx1035) with the [hardware oracle](../HW_ORACLE.md) for 2D images with identity swizzle. They read N texels (BY2: 2, BY4: 4) from `x & ~(N - 1)` on row y, texel by texel and component by component within each texel, with each texel converted like `image_load`. Every register is zero when the mip level is past the last level, `x + N - 1` is outside the level's width (unaligned x, unsigned), or y is outside its height. Data only comes back when the N texels fit in one dword: R8 and R16 (DMASK 0x3) or RG8 (0xf) for BY2, and R8 (0xf) for BY4. With 32-bit texels (R32, RG32, RG16, RGBA8) the hardware returns zeros. Other formats, masks, dimensions, swizzles, D16/A16 and indirect descriptors throw. The stores (0x52, 0x53, 0x5a and 0x5b) decode but throw at translation. Measured on hardware, they write from the unaligned x up to the end of its dword, which is not implemented. -- [2D stores to 1D storage images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp): an `image_store` with DIM 2D through a 1D T# writes texel x and ignores y, as measured on RDNA2 (gfx1035) for y from 0 to 4 and 0xffffffff, single- and four-component formats and a 1x1 8_8_8_8_UNORM T#: the T# type decides the addressing, DIM only how many address VGPRs are read. Storage loads, atomics and `image_store_mip` through such a pair still throw +- [2D stores to 1D storage images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp): an `image_store` with DIM 2D through a 1D T# writes texel x and ignores y, as measured on RDNA2 (gfx1035) for y from 0 to 4 and 0xffffffff, single- and four-component formats and a 1x1 8_8_8_8_UNORM T#: the T# type decides the addressing, DIM only how many address VGPRs are read. An `image_store` with DIM 2D_ARRAY through a 1D T# writes texel x, ignoring y, only when the slice is 0, and is dropped for any other slice: measured on RDNA2 (gfx1036) with the [hardware oracle](../HW_ORACLE.md) through the AMD Windows driver's comgr and HIP runtime (wave32, IEEE off, DX10 clamp on, denormals preserved, round to nearest even) on a 64-texel linear 32_UINT T# with DEPTH 0 and 3, for x of 0, 5 and 63, y of 0, 1, 3 and 0xffffffff and slices 0, 1, 2, 5 and 0xffffffff; the PS5's gfx1013 is not measured. Storage loads, atomics and `image_store_mip` through such a pair still throw - [MIMG PCK2/PCK4 loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_load[_mip]_pck2/pck4`, opcodes 0x70, 0x71, 0x73 and 0x74) were measured on RDNA2 (gfx1035) with the [hardware oracle](../HW_ORACLE.md) for 2D images with identity swizzle. With data mask 0x1, they write one dword: the raw bits of the N texels (PCK2: 2, PCK4: 4) from `x & ~(N - 1)` on row y, concatenated (element k at bit `k * texel bits`) and zero-extended. The dword is zero when the mip level is past the last level, `x + N - 1` is outside the level's width (unaligned x, unsigned), or y is outside its height. Data only comes back when N texels fit in 32 bits: R8, R16 or RG8 for PCK2, R8 for PCK4. Wider texels (R32, RG16, RGBA8) and masks other than 0x1 write zeros on hardware. Those masks throw at decode, and formats outside UNORM/UINT/SINT R8, R16 and RG8 throw. #1314's earlier measurement saw only zeros because it used masks 0x3 and 0xf. The stores (0x76, 0x77, 0x79 and 0x7a) decode but throw at translation. They are not measured, and opcode 0x76 on 32_32_32_32_FLOAT hung the GPU (#1314). - [`image_get_resinfo` through DIM 1D on a cube, 2D-array or 3D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) follows thuanquang's measurement on RDNA2 (RX 6700 XT, gfx1031, #7550), not the PS5's gfx1013: width, height and the level count come from the T#, and the third component is the 3D T#'s depth, or 0 for a cube or 2D array, as for every DIM without layers (1D, 2D, 2D MSAA). Such a query on a 1D-array T# still reads it as its first layer, whose height is not measured. On a 2D MSAA or 2D MSAA-array T# it throws: gfx1031 was measured only with one fragment, which the driver cannot bind, and the level component for 2, 4 or 8 fragments is not measured. - [3D image accesses bound to a 2D or 2D-array T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) follow CouchRiv's [hardware oracle](../HW_ORACLE.md) measurement on RDNA2 (gfx1036, #2023) and adriadam10's (gfx1035, #2140), not the PS5's gfx1013. On a 2D T#, `image_sample_lz` ignores r and reads the (u,v) texel, `image_load` and `image_load_mip` read the (x,y) texel when z is 0 and return zero otherwise, and `image_get_resinfo` returns (width, height, 1, levels). On a 2D-array T#, these accesses behave like dim 2D-array: r or z is the layer, and `image_get_resinfo` returns the layer count as its third component. Every other access of such an image, including samples with an LOD, bias, derivatives, offset or comparison, gathers, `image_get_lod` and stores, keeps throwing because it is not measured. @@ -86,9 +86,9 @@ - [8_SRGB and 8_8_SRGB loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) on a device that cannot sample R8_SRGB or R8G8_SRGB return the sRGB EOTF rounded to nearest even at bf16 precision, as adriadam10 measured on RDNA2 (gfx1035) in #866; the PS5's gfx1013 is not measured. This is the path on a device that cannot blit R8 or RG8 to RGBA8, and with `APS5_SRGB_SHADER_DECODE=1`; otherwise a load of a sampled texture reads the RGBA8 sRGB copy and gets the device's own decode. - [Samples and gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8_SRGB and 8_8_SRGB textures read an RGBA8 sRGB copy of the texture (libSceAgcDriver, [Texture.cpp](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp)) on a device that cannot sample R8_SRGB or R8G8_SRGB but can blit R8 or RG8 to RGBA8, so they use that device's own decode. They still throw on a device that cannot blit, and with `APS5_SRGB_SHADER_DECODE=1`. `APS5_SRGB_SHADER_DECODE=copy` also takes the copy on a device that samples these formats natively, so the copy can be tested on any device that can blit R8 or RG8 to RGBA8; on a device that cannot blit, it behaves like `APS5_SRGB_SHADER_DECODE=1`. A multisampled or depth view of these formats gets no copy. - [Bindless image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) entries of 8_SRGB and 8_8_SRGB textures read zeros, like converted formats, on a device that cannot sample R8_SRGB or R8G8_SRGB. -- [Shader MODE register access](../../../core/shader/recompiler/Translation/src/RoundingMode.cpp) starts from the rounding fields of `GuestContext::floatMode` and follows immediate rounding writes (`s_round_mode`, `s_setreg_imm32_b32` on MODE bits 0-3) through the control flow graph; `s_getreg_b32` of those bits returns the tracked value. Only `v_cvt_f32_i32`, `v_cvt_f32_u32`, `v_ldexp_f32`, the f16 conversions and `v_ldexp_f16` (with f16 denormals kept) and the instructions that already read `FLOAT_MODE` follow a rounding mode set at run time; any other float instruction that runs while a directed rounding mode differs from the initial one throws at that instruction. Rounding bits written from an SGPR (`s_setreg_b32`) or that differ between the paths reaching an instruction throw where they are read or used. A shader without `FLOAT_MODE` metadata throws on any rounding change. Non-rounding register reads still throw; `s_setreg_b32` writes to other hardware registers are still ignored. Tests follow the public RDNA2/LLVM contract, not console measurements. +- [Shader MODE register access](../../../core/shader/recompiler/Translation/src/RoundingMode.cpp) starts from the rounding fields of `GuestContext::floatMode` and follows immediate rounding writes (`s_round_mode`, `s_setreg_imm32_b32` on MODE bits 0-3) through the control flow graph; `s_getreg_b32` of those bits returns the tracked value. Only `v_cvt_f32_i32`, `v_cvt_f32_u32`, `v_ldexp_f32`, `v_cvt_pk_u8_f32`, the f16 conversions and `v_ldexp_f16` (with f16 denormals kept) and the instructions that already read `FLOAT_MODE` follow a rounding mode set at run time; any other float instruction that runs while a directed rounding mode differs from the initial one throws at that instruction. Rounding bits written from an SGPR (`s_setreg_b32`) or that differ between the paths reaching an instruction throw where they are read or used. A shader without `FLOAT_MODE` metadata throws on any rounding change. Non-rounding register reads still throw; `s_setreg_b32` writes to other hardware registers are still ignored. Tests follow the public RDNA2/LLVM contract, not console measurements. - [Flat scalar-read reachability](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) keeps both successors active for non-clean predicates and loop headers, so an inaccessible direct root on such a path still refuses capture. Skipping a read under a clean, evaluable scalar branch follows RDNA2 conditional PC semantics and synthetic fixtures; console execution and title behavior are unverified. -- [v_cvt_pk_u8_f32](../../../core/shader/recompiler/Translation/src/ConvertInstructions.cpp) throws in f32 denormal modes 1 and 2 and with a directed round mode set at run time (see the previous entry); the PS5's gfx1013 is not measured. Otherwise it rounds its source in the `FLOAT_MODE` f32 round mode (ties to even, toward +infinity, -infinity or zero), clamps it to 0..255 (a NaN gives 0) and flushes a denormal source when f32 denormals are flushed. A kept positive denormal gives 1 toward +infinity, decided on the bits, as a driver without `DenormPreserve 32` may flush it. Measured on RDNA2 (gfx1030, RX 6800, AMD Windows driver 32.0.21045.5002) over 2,133 sources (k/8 for k = -16 to 2,099, values next to 0.5 and 255.5, ±1e10, denormals, infinities, NaNs) at IEEE 0 and 1, f32 denormal modes 0 and 3 and f32 round modes 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even, FP16_OVFL=0. +- [v_cvt_pk_u8_f32](../../../core/shader/recompiler/Translation/src/ConvertInstructions.cpp): the PS5's gfx1013 is not measured. It rounds its source in the `FLOAT_MODE` f32 round mode (ties to even, toward +infinity, -infinity or zero), clamps it to 0..255 (a NaN gives 0) and flushes a denormal source when f32 denormals are flushed. A kept positive denormal gives 1 toward +infinity, decided on the bits, as a driver without `DenormPreserve 32` may flush it. Measured on RDNA2 (gfx1030, RX 6800, AMD Windows driver 32.0.21045.5002) over 2,133 sources (k/8 for k = -16 to 2,099, values next to 0.5 and 255.5, ±1e10, denormals, infinities, NaNs) at IEEE 0 and 1, f32 denormal modes 0 and 3 and f32 round modes 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even, FP16_OVFL=0. A round mode set by `s_round_mode` gives the results of the same mode in `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2,148 such sources in every f32 denormal and round mode. - A [flat SRT read](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) through a null or unmapped pointer loaded from memory raises a [BDA fault](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp) only if the program executes it, then stops or reads zero like a BDA read. - Repeated `s_load`s of one [flat SRT](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) dword through a null or unmapped pointer report one of their pcs, and an `s_load` whose inaccessible dwords are all unused, or only address repeated loads, reports no fault of its own. - A value read through a null or unmapped pointer still throws at record time if a [uniform fill or table base](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) needs it, or if the pointer comes from user data and is not null, even if the console skips the read. @@ -126,7 +126,7 @@ - [f16 unary, frexp and conversion denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_ceil_f16`, `v_floor_f16`, `v_fract_f16`, `v_rcp_f16`, `v_rsq_f16`, `v_sqrt_f16`, `v_log_f16`, `v_exp_f16`, `v_sin_f16`, `v_frexp_mant_f16`, `v_frexp_exp_i16_f16`, `v_cvt_norm_i16_f16`, `v_cvt_norm_u16_f16` and `v_cvt_f32_f16` flush a denormal half input to its signed zero when bit 0 is clear (0 and 2), and the f16-result ones flush a denormal half result to its signed zero when bit 1 is clear (0 and 1), after the host rounds it to half. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with denorm16 0..3 over every non-NaN f16 input (63,490 rows per mode and instruction): the only mismatches are the 1-ULP driver differences of `v_exp_f16` (2 inputs) and `v_sin_f16` (14), which are the same with denormals kept. NaN inputs are not compared. `v_ldexp_f16` shares the input rule. `v_cvt_pknorm_i16_f16` and `v_cvt_pknorm_u16_f16` flush both half inputs by the same bit, measured on RDNA2 (gfx1030) over every f16 input (each half also negated in the other source) at IEEE=0 and IEEE=1 with denorm16 0..3 and at f16 rounding 1..3: every row matches. Shaders without a known float mode keep the previous lowering. - [v_div_fixup_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) flushes its src1 and src2 to a zero of the same sign when bit 0 of the `FLOAT_MODE` f16 denormal field is clear (0 and 2) and keeps a denormal src0 and result in every mode, as `v_div_fixup_f32` does with the f32 field. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 6859 triples of 19 halves (zeros, denormals, minimum normals, ±0.5, ±1, the largest finite value, infinities and quiet and signaling NaNs) at IEEE=0 and IEEE=1 with denorm16 0..3 and FP16_OVFL 0 and 1, also with neg and abs source modifiers and with op_sel high halves. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [Packed f16 NaN results](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding throws. In the f16 denormal modes that flush, each denormal input lane is flushed to a zero of its sign when bit 0 of the field is clear (0 and 2), so `inf * 0x0001` is an invalid product there, and when bit 1 is clear (0 and 1) a `v_pk_add_f16` result below 2^-14, or a `v_pk_mul_f16`, `v_pk_fma_f16` or `v_pk_fmac_f16` result whose exact value rounded to 11 significant bits with an unbounded exponent is below 2^-14, is flushed to a zero of its sign; `v_pk_min/max_f16` flush only their inputs. The native f16x2 add and multiply is not used while results are flushed. 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 19,192 to 162,204 lanes per instruction with op_sel, op_sel_hi, neg_lo, neg_hi and clamp variants and products and sums at the 2^-14 boundary. -- [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; Its f16 sources are flushed to a zero of their sign when bit 0 of the f16/f64 denormal field is clear (0 and 2), whatever the f32 field, measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with f32 denormal modes 0 and 3 and f16 denormal modes 0 to 3, over 2744 triples of 14 halves with all three sources f16 and 1960 rows with an f32 multiplicand. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions except the f32 compares. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` and the image, buffer, global and flat `fmin/fmax/fcmpswap` flush both operands by the field, see the float atomics entries. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. +- [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; Its f16 sources are flushed to a zero of their sign when bit 0 of the f16/f64 denormal field is clear (0 and 2), whatever the f32 field, measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with f32 denormal modes 0 and 3 and f16 denormal modes 0 to 3, over 2744 triples of 14 halves with all three sources f16 and 1960 rows with an f32 multiplicand. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions except the f32 compares and the f32 conversions in their entries. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` and the image, buffer, global and flat `fmin/fmax/fcmpswap` flush both operands by the field, see the float atomics entries. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. - [v_rcp_f32 and v_rcp_iflag_f32 with `APS5_EXACT_RCP`](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp) return the hardware reciprocal for normal inputs with normal results: a cubic over 32 mantissa segments, the linear term rounded to nearest even, plus 2,761 one-ULP corrections. It matches all 2^23 mantissas of an RDNA2 (gfx1031) sweep; inputs or results that are not normal keep the driver's 1/x. It is off by default because the coefficient and correction tables add about 13k SPIR-V words to each shader that uses it. The switch also reaches v_rcp_f16 (rounded from the f32 reciprocal) and the pixel shader's position W input. - [v_ldexp_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f32 denormal field: mode 1 keeps a subnormal input and flushes a result below 2^-126 to a zero of its sign in every f32 round mode, also one that rounds to 2^-126 at subnormal precision (`0x3fffffff` scaled by 2^-127); mode 2 flushes a subnormal input to a zero of its sign and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, as mode 3 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 10,260 rows (54 inputs of both signs: zeros, subnormals, normals near 2^-126 and 1.0, the largest finite value, infinities and NaNs, each scaled by 190 exponents from -2^31 to 2^31-1) at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3 and f32 round mode 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0; with the `clamp` modifier the result is clamped as in the other modes. The PS5's gfx1013 is not measured. @@ -137,6 +137,7 @@ - [Ordered not-equal, less-than and greater-than compares](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler - [16-bit integer multiplies](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`v_mul_lo_u16`, `v_pk_mul_lo_u16`) (shader recompiler) - a factor taken from the low half of its source is read as the low 24 bits of the register, not the low 16; the low 16 bits of the product are the same for every input. The AMD Windows driver 32.0.21045.5002 (RX 6700 XT, RDNA 2) stores the full 32-bit product when two values it knows fit in 16 bits are multiplied, the product is reduced to 16 bits and nothing else is merged into the stored value: `v_mul_lo_u16` into a register whose high half is zero, also with factors masked to 16 bits by the program, and `v_pk_mul_lo_u16` whose high lanes are zero wrote 0xfffe0001 for 0xffff * 0xffff where the GPU writes 0x00000001. Reducing the product with `v_and_b32`, `v_bfe_u32` or a shift pair loses the same bits; factors read as 24-bit fields do not. The [constant folder](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) keeps a field read that a multiply uses, even when the program already masked the factor to 16 bits, since folding it away brings the full product back (#8271). Measured against the same instructions run through the [hardware oracle](../HW_ORACLE.md) on that GPU; other drivers are not checked - [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_div_scale_f32` and `v_div_fixup_f32` do not follow this rule and still throw in modes 1 and 2, as do `v_mad_mixlo/mixhi_f16`, which RDNA2 lacks. The PS5's gfx1013 is not measured. - [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. @@ -189,7 +190,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. -- [SDWA source selects on `v_cvt_f32_ubyte1`, `v_cvt_f32_ubyte2` and `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, since SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads `S0.u[8N+7:8N]` (12.8). Not measured on hardware. +- [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. - [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 - [Shared loop latches](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a loop whose single latch holds instructions and is reached by one edge gets an empty continue block, so its continue construct is that one block. A latch that several `continue` paths share keeps its instructions in the continue construct, since isolating it would clone them into each path. When such a latch emits more than one SPIR-V block, as a memory or image access does, SPIRV-Cross (MoltenVK) folds the construct into a `for` increment and drops the header's `OpPhi` updates, so the loop never advances on Metal. No title is known to reach this case. - [Scalar buffer descriptors](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) use only the base address, stride and record count for resources accessed exclusively by scalar loads; their fourth word is normalized to zero, following AMD RDNA2 ISA section 7.2.2. Vector and mixed accesses retain descriptor validation. Native Vulkan readbacks cover ignored fourth-word fields on Windows/NVIDIA; their console behavior has not been measured. diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 98970b5163..05c49f2559 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -131,3 +131,27 @@ MouseCurve = 1.3 Earlier versions sampled the mouse every 33 ms and tilted the stick at least halfway for any movement. `MouseDeadzoneOffset = 0.5`, `MouseSmoothingMs = 0` and `MouseDecayMs = 33` come close to that behavior. An invalid line reports the file and line number and stops input initialization. If the configured file does not exist or cannot be read, AnyPS5 reports an error. With no `anyps5-input.ini` and no `ANYPS5_INPUT_CONFIG`, the built-in mapping is used. + +## Web configurator + +The [input mapper](../../tools/input-mapper/index.html) edits keyboard, mouse and wheel overrides in the browser. It supports alternate bindings, physical-key capture, search, INI import, preview, copy and download. Invalid imports preserve the current configuration. Invalid bindings block export; duplicate and shared inputs, including retained built-in bindings, produce warnings. Removing every binding for an action restores its built-in inputs. + +From the repository root, with Node.js 22 or later: + +```sh +npm --prefix tools/input-mapper start +``` + +Open `http://127.0.0.1:4173`. Stop the server with Ctrl+C. No package installation is required. Set `PORT` to use another port. Run the tests with: + +```sh +npm --prefix tools/input-mapper test +``` + +Controls, Configuration and Help are separate views; switching views preserves bindings. Search covers all control groups. The group selector supports arrow keys, Home and End. Input and language menus support search, arrow keys and Enter; Escape cancels selection. Menus open beside their fields on larger screens and as bottom sheets on phones. The interface uses system fonts and respects reduced-motion preferences. + +The translation button selects one of 22 interface and FAQ languages. First visits use English; a saved language choice takes priority. Egyptian Arabic appears first in the language list. Arabic, Urdu and Persian use right-to-left layouts. Only the language preference is persisted; copy or download input changes before reloading. + +Keyboard capture converts physical browser key codes to SDL scancode names. Escape cancels capture; select Escape manually to assign it. Composition, ambiguous keys and some browser or system shortcuts require manual selection. Names containing `#` or `;` cannot be exported because those characters start INI comments. The key catalogue follows SDL commit `4b69833bc54abf3dd3288d4aa7afbba527775e5b`. The source-contract tests compare actions, restrictions and defaults with the current native input code. + +For static hosting, serve `index.html`, `styles.css`, `app.js`, `mapping.js`, `keys.js`, `capture.js`, `validation.js`, `i18n.js` and `dropdown.js` from one directory. The configurator is not published on the project's GitHub Pages site yet. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 3a4dc187ee..bba0b4d542 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -29,6 +29,8 @@ relinker [options] An explicit `--module-dir` also works when the module parent has no `sce_module`, `sce_modules` or `prx` directory. For a homebrew bundle with shared libraries beside its executable, use `--module-dir .`; no empty standard directory is required. Without an explicit module directory, the standard-directory requirement remains. +A module the executable needs that is not converted is looked for under the module parent directory, outside the module directories. Each SELF container or `ET_SCE_DYNAMIC` ELF found there whose name matches, up to the `.prx`, `.sprx`, `.suprx` or `.debug_prx` suffix (ignoring case with `--windows`, as the guest module match does), is reported after the conversion with the `--module-dir` that would include it; the output still refers to a host library of that name. A file that cannot be read is not reported. + Linux output: ```sh @@ -64,8 +66,8 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--macos` | Produce an x86-64 Mach-O executable, with bundled modules as Mach-O libraries in `app0/sce_module`. Conflicts with `--windows`. | | `--windows-diagnostics` | Include startup dependency diagnostics. Requires `--windows`. | | `--windows-gui` | Select the Windows GUI subsystem instead of the console subsystem. Requires `--windows`. | -| `--windows-fixed-base
` | Map the lowest ELF load page at this fixed Windows address, given in decimal or hexadecimal (`0x`). Requires `--windows`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and a complete image fitting in the user address space. The main executable has no ASLR or base relocations; an occupied address fails to load. Bundled modules keep their normal placement. | -| `--trace-imports` | Pass each function import through the libc.prx call tracer, see [tracing system library calls](#tracing-system-library-calls). Windows and Linux output only; conflicts with `--lazy-binding`. | +| `--windows-fixed-base
` | Map the lowest ELF load page at this fixed Windows address instead of the preferred `0x400000`, given in decimal or hexadecimal (`0x`). Requires `--windows`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and a complete image fitting in the user address space. The main executable has no ASLR or base relocations; an occupied address fails to load. Bundled modules keep their normal placement. | +| `--trace-imports` | Pass each function import through the libc.prx call tracer, see [tracing system library calls](#tracing-system-library-calls). Windows, Linux and macOS output; conflicts with `--lazy-binding`. | | `--linux-fixed-base
` | Load the Linux executable at this fixed address instead of the default `0x400000`, given in decimal or hexadecimal (`0x`): every virtual address of the image is offset by it, so virtual address 0 is placed at the address, as the console places an executable. Requires a 64 KiB-aligned address from `0x10000` through `0x7fffffff0000` that is a multiple of every `PT_LOAD` alignment, and an image that ends in the user address space. The executable is written as `ET_EXEC`, without ASLR, as it is at the default address. Bundled modules keep their normal placement. Conflicts with `--windows`, `--macos` and `--lazy-binding`. | | `--macos-fixed-base
` | Map the lowest ELF load page of the macOS executable at this fixed address, given in decimal or hexadecimal (`0x`). Requires `--macos`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and an image that ends in the macOS user address space. `__PAGEZERO` ends 64 KiB below the address and the Mach-O header occupies those 64 KiB. The executable has no ASLR; an address the process cannot map fails to load. Bundled modules keep their normal placement. | | `--to-intel` | Convert supported AMD-only instructions in the executable and bundled modules. Unsupported instructions or unreachable conversion stubs cause an error. | @@ -74,7 +76,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `unused-filter=1` | Filter unused non-PLT imports using control-flow and GOT access analysis, treating address-taken code as reachable; preserve PLT imports. | | `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. Linux guest modules require an absolute path or a path beginning with `$ORIGIN`. Windows requires a nonempty ASCII path and supports `$ORIGIN` as the executable directory. | +| `--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. | | `--autorun` | Run the output after conversion, print its exit code, and wait for Enter. Adds executable permissions for Linux output. 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`). 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`. | @@ -103,7 +105,7 @@ A patch file changes bytes of the input executable or of a bundled module before The advisor requires Python 3.11 or newer. To check a local patch catalog, run: ```sh -python3 tools/suggest_patches.py --catalog --param-json --input --relinker [--sce-module-path ] [--module-dir ]... [--executable ]... [--skip-sce-module] [--timeout-seconds ] +python3 tools/suggest_patches.py --catalog --param-json --input --relinker [--sce-module-path ] [--module-dir ]... [--executable ]... [--skip-sce-module] [--timeout-seconds ] [--list-verified] ``` The advisor reads `titleId` and `contentVersion` from the specified `param.json`, checks the SHA-256 of every file targeted by a candidate patch, then calls the relinker specified by `--relinker` to run `--verify-patches`. A relative path such as `--relinker ./relinker` resolves to that local file. The explicit metadata path also works when `param.json` and the unwrapped ELF files are stored separately. The catalog directory must contain an `index.json` with `format: 1` and an `entries` array. Each entry has `titleId`, `contentVersion`, a relative `patch` path, and `targets` mapping every patch target to its SHA-256. Hexadecimal digests are case-insensitive. For example: @@ -120,7 +122,9 @@ The advisor reads `titleId` and `contentVersion` from the specified `param.json` } ``` -The advisor validates patch files only for entries matching the requested title and version. It reports and skips a malformed matching entry or a relinker verification error, so another valid entry can still be recommended. Verification times out after 120 seconds per entry by default; use `--timeout-seconds ` to change the limit. For synthetic inputs without system modules, use `--skip-sce-module` if conversion also uses it; this option conflicts with `--sce-module-path`. No matching entry or no verified patch exits `1`; invalid input or no verified patch after a relinker error exits `2`. If multiple entries verify, the advisor recommends none because it cannot choose or combine them. The tool does not fetch or apply patches. A recommendation prints `--patches` and the module and executable options to use when converting. Keep catalog patch files outside the input executable's directory to avoid automatic `relinker-patches.toml` discovery. +The advisor validates patch files only for entries matching the requested title and version. It reports and skips a malformed matching entry or a relinker verification error, so another valid entry can still be recommended. Verification times out after 120 seconds per entry by default; use `--timeout-seconds ` to change the limit. For synthetic inputs without system modules, use `--skip-sce-module` if conversion also uses it; this option conflicts with `--sce-module-path`. No matching entry or no verified patch exits `1`; invalid input or no verified patch after a relinker error exits `2`. If multiple entries verify, the advisor recommends none because it cannot choose or combine them, and points to `--list-verified` to inspect the matches. The tool does not fetch or apply patches. A recommendation prints `--patches` and the module and executable options to use when converting. Keep catalog patch files outside the input executable's directory to avoid automatic `relinker-patches.toml` discovery. + +With `--list-verified`, the advisor prints every entry that passes metadata, hash and relinker checks, in catalog order. Each result includes its catalog number, patch file, target hashes and conversion options. It exits `0` when at least one entry verifies, even if another entry was rejected; with no verified entries, the existing `1` or `2` exit status still applies. Without this option, a single verified entry is recommended and multiple verified entries remain ambiguous. The listing tells users to choose one patch file. It does not combine patch files or apply them during conversion; review the chosen patch before applying it. The target hashes identify the exact unwrapped files changed by a patch, which might not include the main executable. Different dump tools could produce different bytes for the same title version; the advisor then rejects a patch even if it would work. It has not been confirmed that all dump tools produce identical files. The hashes do not authenticate a catalog or establish that a patch is safe or that the title is fully playable. Use catalogs from sources you trust and review the patch before applying it. @@ -176,7 +180,7 @@ Verify mode rejects the conversion options (`--windows`, `--macos`, `--to-intel` ## Runtime layout -For an executable that assumes its image starts at `0x400000`, use `relinker --windows --windows-fixed-base 0x400000 source/input.elf app.exe`. This maps its lowest ELF load page there; the PE headers occupy the preceding 64 KiB. Use this option only when the executable requires an exact address. Without it, the Windows executable has no `DYNAMIC_BASE` and loads at its preferred base `0x140000000`; relocations are kept, so Windows relocates it if that range is taken. Modules keep `DYNAMIC_BASE`. This option does not change macOS output. +The Windows executable has no `DYNAMIC_BASE` and prefers to load with its lowest ELF load page at `0x400000`, where Ruffle (PPSA68091), VLC (PPSA85300) and XPSemu (PPSA97358) expect their image when they read their `app0/imports.txt`; the PE headers occupy the preceding 64 KiB (preferred base `0x3f0000`). Relocations are kept, so Windows can relocate it if that range is taken. When the executable requires an exact address, use `relinker --windows --windows-fixed-base
source/input.elf app.exe`, which maps the lowest ELF load page at that address without relocations, so an occupied address fails to load. Modules keep `DYNAMIC_BASE`. This option does not change macOS output. For an executable that assumes its image starts at `0x400000`, as Ruffle (PPSA68091), VLC (PPSA85300) and XPSemu (PPSA97358) do when they read their `app0/imports.txt`, use `relinker --macos --macos-fixed-base 0x400000 source/input.elf app` on macOS. Use this option only when the executable requires a fixed address; without it, macOS output places the lowest ELF load page at `0x140010000`. Linux output places virtual address 0 at `0x400000` without an option, inside the 40-bit window that a title's own `sceLibcMspaceCreate` checks; `--linux-fixed-base` selects another address, and with `--lazy-binding` the executable stays position independent. @@ -201,7 +205,7 @@ Some applications also need their original `app0/sce_sys/param.json` at runtime. Resources can also be packed into `app0.zar` beside `app0/`, a [ZArchive](https://github.com/Exzap/ZArchive) whose root is the `app0/` directory. A file present in `app0/` is read from there and any other file from the archive; the executable, `sce_module/` and `sce_sys/` stay in `app0/`. The archive is read only through `sceKernelOpen`/`open`, the read, seek and stat calls on its descriptors and `sceKernelStat`; other calls on an archived path, listing a directory that the archive also contains and opening a file in it for writing fail as described in [technical debt](../dev/debt/libkernel.md). An archive that cannot be opened stops the title at its first file access. -Use the generated files printed as `Guest module:` for bundled title modules. `libs/` is for AnyPS5 system libraries, not the original PS5 `.prx` files. Placing an original PS5 module in `libs/` on Windows makes Windows try to load it as a DLL and can fail with error 193 (not a valid Win32 application). A module the title loads while it runs that was not relinked, such as one in a directory not given to `--module-dir`, is not loaded either: the title gets an error and continues, and the runtime prints `[module] dlopen: was not relinked: ...` once for each file. Relink that module's directory with `--module-dir `. +For bundled title modules, use the `.guest.prx` files the relinker writes into `app0/` and lists in the layout it prints. `libs/` is for AnyPS5 system libraries, not the original PS5 `.prx` files. Placing an original PS5 module in `libs/` on Windows makes Windows try to load it as a DLL and can fail with error 193 (not a valid Win32 application). A module the title loads while it runs that was not relinked, such as one in a directory not given to `--module-dir`, is not loaded either: the title gets an error and continues, and the runtime prints `[module] dlopen: was not relinked: ...` once for each file. Relink that module's directory with `--module-dir `. On Windows, a self-built `libs/` also needs `libgcc_s_seh-1.dll`, `libstdc++-6.dll` and `libwinpthread-1.dll` from the `mingw64/bin` directory of the toolchain that built the libraries; the release archives already contain them. The libraries are loaded without searching `PATH`, so a copy elsewhere on the system is not used, and a library that needs one of them fails with error 126 (the specified module could not be found) although its `.prx` file is present. With `APS5_MIMALLOC` set, the title loads `mimalloc.dll` and `mimalloc-redirect.dll` from `libs/` and allocates through mimalloc. Copy both from `build/core/libs/libs`; the release archives contain them. Without them the title keeps the C runtime's heap and prints nothing. @@ -270,9 +274,9 @@ A game unlocks a trophy by posting a `_UnlockTrophy` event with an int32 `_troph The trophy data comes from the game's own `app0/sce_sys/trophy2/trophyNN.ucp`, where `NN` is the service label (`00` for most games). The trophy queries report its names, descriptions and rewards in the console language (`ANYPS5_LANGUAGE`, see above), or in English (United States) or the data's default language when it has no text in that language, and its grades, groups, hidden flags, number of trophies and grade counts. A platinum trophy unlocks as soon as every trophy that names it in `platinumTrophyId` is unlocked. The first trophy query or event prints what was read, such as `[trophy] /app0/sce_sys/trophy2/trophy00.ucp: 52 trophies, 1 group, texts in en-US`. -Many games unlock trophies through statistics instead: `app0/sce_sys/uds/udsNN.ucp` names, for each event, the property whose value becomes a statistic, and a trophy's `unlockCondition` compares a statistic with a target. The latest value of each statistic is stored in `_trophy/_.stats.txt`, one ` ` line per statistic, and is kept across runs; delete it together with the unlock file to start over. A progressive trophy reports its statistic as progress. Only statistics that keep the latest value (`"aggregation":"latest"`) as `uint32` or `uint64`, and rules that copy one property of an event chosen by its name (`$.name` or `$.object.name`), are applied; the others are listed in `[trophy]` lines when the file is read. +Many games unlock trophies through statistics instead: `app0/sce_sys/uds/udsNN.ucp` names, for each event, the property whose value becomes a statistic, and a trophy's `unlockCondition` compares a statistic with a target. The latest value of each statistic is stored in `_trophy/_.stats.txt`, one ` ` line per statistic, and is kept across runs; delete it together with the unlock file to start over. A progressive trophy reports its statistic as progress. Only statistics that keep the latest (`"aggregation":"latest"`) or the largest value (`"max"`) as `uint32`, `uint64` or `int32`, and rules that copy one property of an event chosen by its name (`$.name` or `$.object.name`), optionally only when one property equals an integer (`_UpdateTrophyProgress` by its `_trophy_id`), are applied; the others are listed in `[trophy]` lines when the file is read. -`APS5_TRACE_UDS=1` prints every posted event, such as `[uds] context 1 posted _UnlockTrophy {"_trophy_id":12}`, every statistic an event sets, such as `[uds] stat 10 of user 268435456 is 10 (COLLECT $.counter)`, and a `[uds]` line for a missing `trophyNN.ucp` or `udsNN.ucp`. An event that is not used prints the reason in a `[trophy] ignored` line: an `_UnlockTrophy` event without an int32 `_trophy_id` of 0 or more, or posted to a context that does not exist; the first `_UpdateTrophyProgress` event of each trophy, because that event is not interpreted; and, once per rule, an event whose property is missing, negative, not an integer or outside the statistic's range. +`APS5_TRACE_UDS=1` prints every posted event, such as `[uds] context 1 posted _UnlockTrophy {"_trophy_id":12}`, every statistic an event sets, such as `[uds] stat 10 of user 268435456 is 10 (COLLECT $.counter)`, and a `[uds]` line for a missing `trophyNN.ucp` or `udsNN.ucp`. An event that is not used prints the reason in a `[trophy] ignored` line: an `_UnlockTrophy` event without an int32 `_trophy_id` of 0 or more, or posted to a context that does not exist; and, once per rule, an event whose property is missing, negative, not an integer or outside the statistic's range. A missing or malformed `trophyNN.ucp` or `udsNN.ucp` turns off only what it provides, and a malformed one prints the reason once: without trophy data the queries report placeholders and statistics unlock nothing, and without statistics rules events change no statistic. Icons are not read: `sceNpTrophy2GetGameIcon`, `sceNpTrophy2GetGroupIcon`, `sceNpTrophy2GetTrophyIcon` and `sceNpTrophy2GetRewardIcon` stop the game with an error. So does an unlock or statistics file in `_trophy/` that is damaged (a line that is not a new ` ` pair, such as a line an edit left blank or ended with a carriage return) or cannot be read or written. No notification is shown, and callbacks registered with `sceNpTrophy2RegisterUnlockCallback` are not called. @@ -318,7 +322,7 @@ Recompiled shaders and the Vulkan pipeline cache are kept in a `shader_cache/` d ### Pipeline journal -Besides the recompiled shaders, the shader cache directory holds `pipelines.journal`: every Vulkan graphics pipeline the pipeline store kept and every compute pipeline a run built, with the specialized shader modules they were built from. The next run of the title builds those pipelines on background threads while it loads, so a draw or dispatch the earlier run already made does not compile its pipeline when it is first reached. The journal belongs to the GPU and driver that wrote it, like the driver's pipeline cache file next to it: a run on another GPU or driver version starts it over, and the driver's own pipeline cache decides how fast each pipeline is rebuilt. Two runs sharing the directory take turns on `pipelines.journal.lock`; a run that cannot lock it records nothing. `APS5_NO_PIPELINE_JOURNAL=1` disables recording and replay; `APS5_PIPELINE_JOURNAL_THREADS` sets the replay threads (half the CPU threads by default; `0` records without replaying), `APS5_PIPELINE_JOURNAL_ENTRIES` how many pipelines are replayed (the first recorded, which a run reaches first) and `APS5_PIPELINE_STORE_ENTRIES` how many graphics pipelines stay resident (1024 each), and `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024) the file size at which recording stops. Deleting the file loses nothing but the head start. With `APS5_PROFILE_DRAW=1` a `[pipeline-journal]` line reports what was loaded, replayed and recorded. +Besides the recompiled shaders, the shader cache directory holds `pipelines.journal`: every Vulkan graphics pipeline the pipeline store kept and every compute pipeline a run built, with the specialized shader modules they were built from. The next run of the title builds those pipelines on background threads while it loads, so a draw or dispatch the earlier run already made does not compile its pipeline when it is first reached. The journal belongs to the GPU and driver that wrote it, like the driver's pipeline cache file next to it: a run on another GPU or driver version starts it over, and the driver's own pipeline cache decides how fast each pipeline is rebuilt. Two runs sharing the directory take turns on `pipelines.journal.lock`; a run that cannot lock it records nothing. `APS5_NO_PIPELINE_JOURNAL=1` disables recording and replay; `APS5_PIPELINE_JOURNAL_THREADS` sets the replay threads (half the CPU threads by default; `0` records without replaying), `APS5_PIPELINE_JOURNAL_ENTRIES` how many pipelines are replayed (4096, the last recorded) and `APS5_PIPELINE_STORE_ENTRIES` how many graphics pipelines stay resident (by default the store grows from 1024 up to 4096 to hold the replay and the pipelines that keep coming back), and `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024) the file size at which recording stops. Deleting the file loses nothing but the head start. With `APS5_PROFILE_DRAW=1` a `[pipeline-journal]` line reports what was loaded, replayed and recorded. ### Draw pipeline @@ -344,7 +348,7 @@ To measure how a title runs on a machine, start it with `APS5_TRACE_SUMMARY_EVER ### Tracing system library calls -Windows and Linux. 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, which has 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. 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): @@ -355,6 +359,13 @@ most called traced imports (1 calls in total): 1 libkernel.prx k+AXqu2-eBc ``` +To list every import a title calls during a run that ends without a crash, also set `APS5_TRACE_CALLS_FILE` to a file path. Every 10 seconds the file is rewritten with one line per import called so far, most called first: the number of calls, the library and the import name. It survives a run that is killed, up to its last rewrite. + +``` +245557 libc.prx gQX+4GDQjpM#guest +3866 libSceAgc.prx 57labkp+rSQ +``` + Limits are listed in the [libc](../dev/debt/libc.md) and [relinker](../dev/debt/relinker.md) technical debt. ### Performance overlay @@ -441,3 +452,13 @@ Registry `nid` fields may contain either encoded NIDs or ordinary ELF symbol nam Exit codes: `0` nothing blocks loading, `1` there are `absent` imports or needed libraries without a file, `2` an input could not be read; the message names the file and the value. A module registry lists `nid`, `library` and `targetOffset` for each relocation against a function that no other bundled module exports, without the `#` suffix of the executable's NIDs. + +## Patching imports of native applications + +`nid_patcher --exec [file2 ...]` patches imports in place and preserves the application's exports. It detects PE, ELF64 and Mach-O64 from file contents. No library name is required. Build a separate executable for each target platform. + +By default, imports use the library export naming rules: `_nid_no_patch` and `SDL_*` stay unchanged; `_nid_no_patch_cut` is removed without hashing; `_nid_postfix` and numeric `_nid_disambig` suffixes are removed before computing the NID. When both `foo` and `foo_nid_postfix` are imported, `foo` stays unchanged. These rules also apply to host imports unless they are explicitly preserved. + +`nid_patcher --exec --only-sce ` restricts renaming to names starting with `sce` (case-insensitive) or ending with `_nid_postfix`. Both modes accept `--preserve-exports ` or `--preserve-export-list ` for explicit exclusions. The existing `nid_patcher ` mode is unchanged. + +Unsupported layouts fail before the input file is written. diff --git a/tools/audit_dump.py b/tools/audit_dump.py new file mode 100644 index 0000000000..3653731d05 --- /dev/null +++ b/tools/audit_dump.py @@ -0,0 +1,177 @@ +import argparse +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import import_audit +import unwrap_self + +DESCRIPTION = """Audit the imports of an extracted title before it is launched. Unwraps the dump's eboot.bin and the modules +it ships (sce_module/, sce_modules/ and prx/, Unity's Media/Modules/ and Media/Plugins/, and .prx files beside +eboot.bin) into a work directory, converts them with relinker --registry, and runs tools/import_audit.py on the +registries with the unwrapped modules as --modules. The dump is only read; the source/ and output/ directories of a +reused --work directory are emptied first. Exit code: that of import_audit.py, or 2 +when unwrapping or conversion fails.""" + +STANDARD_DIRECTORIES = ("sce_module", "sce_modules", "prx") +EXTRA_DIRECTORIES = ("Media/Modules", "Media/Plugins") +MODULE_SUFFIXES = (".prx", ".sprx") +ELF_MAGIC = b"\x7fELF" + + +def stage(source, destination): + try: + unwrap_self.unwrap(source, destination) + return "unwrapped" + except unwrap_self.Refused: + with open(source, "rb") as file: + if file.read(4) != ELF_MAGIC: + raise + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source, destination) + return "copied" + + +def modules_of(directory): + return sorted(path for path in directory.iterdir() if (path.is_file() or path.is_symlink()) and path.suffix.lower() in MODULE_SUFFIXES) + + +def prepare(dump, source, log=print): + stage(dump / "eboot.bin", source / "eboot.elf") + module_directories = [] + extra = [] + for relative in STANDARD_DIRECTORIES + EXTRA_DIRECTORIES: + directory = dump / relative + if not directory.is_dir(): + continue + modules = modules_of(directory) + if not modules: + continue + for module in modules: + stage(module, source / relative / module.name) + module_directories.append(source / relative) + if relative in EXTRA_DIRECTORIES: + extra.append(relative) + beside = modules_of(dump) + if beside: + for module in beside: + stage(module, source / module.name) + module_directories.append(source) + extra.append(".") + log(f"staged eboot.bin and {sum(len(modules_of(directory)) for directory in module_directories)} modules") + return module_directories, extra + + +def convert(relinker, source, output, extra, windows): + command = [str(relinker), "--registry"] + if windows: + command.append("--windows") + for relative in extra: + command += ["--module-dir", relative] + command += [str(source / "eboot.elf"), str(output)] + return subprocess.run(command, capture_output=True, text=True) + + +def comparison_path(path, links=frozenset()): + for prefix in (*reversed(path.parents), path): + if not prefix.is_symlink(): + continue + identity = prefix.parent.resolve() / prefix.name + if identity in links: + raise RuntimeError("symbolic link cycle") + target = prefix.readlink() + destination = target if target.is_absolute() else prefix.parent / target + resolved_target = comparison_path(destination, links | {identity}) + return comparison_path(resolved_target / path.relative_to(prefix), links) + resolved = path.resolve() + drive = resolved.drive + if drive.casefold().startswith("\\\\?\\unc\\"): + return type(resolved)("\\\\" + str(resolved)[8:]) + if len(drive) == 6 and drive.startswith("\\\\?\\") and drive.endswith(":"): + return type(resolved)(str(resolved)[4:]) + return resolved + + +def report_overlaps_dump(report, dump): + destination = comparison_path(report) + dump_path = comparison_path(dump) + if report.absolute().is_relative_to(dump.absolute()) or destination.is_relative_to(dump_path) or comparison_path(report.parent).is_relative_to(dump_path): + return True + exists = report.exists() + for path in dump.rglob("*"): + if path.is_symlink() and path.is_dir() and destination.is_relative_to(comparison_path(path)): + return True + if exists and path.is_file() and report.samefile(path): + return True + return False + + +def main(argv=None): + parser = argparse.ArgumentParser(description=DESCRIPTION, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("dump", help="the title's extracted app0 directory, holding eboot.bin") + parser.add_argument("--relinker", required=True, help="the relinker executable") + parser.add_argument("--libs", action="append", required=True, help="built .prx directory, as for import_audit.py (repeatable)") + parser.add_argument("--work", help="directory for the unwrapped files and registries (default: a temporary one, removed afterwards)") + parser.add_argument("--windows", action="store_true", help="convert for Windows, as the title would be run there") + parser.add_argument("--json", help="also write import_audit.py's JSON report to this file outside the input dump") + args = parser.parse_args(argv) + dump = Path(args.dump) + if not (dump / "eboot.bin").is_file(): + parser.error(f"{dump}: no eboot.bin") + relinker = Path(args.relinker).resolve() + if not relinker.is_file(): + parser.error(f"{args.relinker}: no such relinker") + dump_path = comparison_path(dump) + if args.json: + report = Path(args.json) + try: + overlaps = report_overlaps_dump(report, dump) + except (OSError, RuntimeError) as error: + parser.error(f"{report}: cannot check JSON output against the input dump: {error}") + if overlaps: + parser.error(f"{report}: JSON output overlaps the input dump") + with tempfile.TemporaryDirectory() as temporary: + work = Path(args.work) if args.work else Path(temporary) + source, output = work / "source", work / "output" + for generated in (source, output): + generated_path = comparison_path(generated) + if dump_path.is_relative_to(generated_path) or generated_path.is_relative_to(dump_path): + parser.error(f"{generated}: work directory overlaps the input dump") + for generated in (source, output): + try: + if generated.is_symlink(): + generated.unlink() + elif generated.exists(): + shutil.rmtree(generated) + except OSError as error: + print(f"FAIL: cannot empty {generated}: {error}", file=sys.stderr) + return 2 + if generated.exists() or generated.is_symlink(): + print(f"FAIL: cannot empty {generated}: it still exists", file=sys.stderr) + return 2 + output.mkdir(parents=True, exist_ok=True) + try: + module_directories, extra = prepare(dump, source) + except (unwrap_self.Refused, OSError) as error: + print(f"FAIL: unwrapping {dump}: {error}", file=sys.stderr) + return 2 + result = convert(relinker, source, output / ("app.exe" if args.windows else "app.elf"), extra, args.windows) + if result.returncode != 0: + print(f"FAIL: relinker exited with {result.returncode}", file=sys.stderr) + print((result.stderr or result.stdout).strip(), file=sys.stderr) + return 2 + registries = sorted(str(path) for path in output.rglob("*.registry.json")) + audit = registries + [option for library in args.libs for option in ("--libs", library)] + audit += [option for directory in module_directories for option in ("--modules", str(directory))] + if args.json: + audit += ["--json", args.json] + return import_audit.main(audit) + + +if __name__ == "__main__": + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") + sys.exit(main()) diff --git a/tools/build_web.py b/tools/build_web.py index 8a5e555bf0..4127eaf9a0 100644 --- a/tools/build_web.py +++ b/tools/build_web.py @@ -71,7 +71,8 @@ def publish_libraries(output, release=None): raise ValueError("Invalid library archive") names = {} for file in files: - if not file.filename.startswith("libs/") or any(part in ("..", ".", "") for part in file.filename.rstrip("/").split("/")) or "\\" in file.filename: + if (file.orig_filename != file.filename or not file.filename.startswith("libs/") + or any(part in ("..", ".", "") for part in file.filename.rstrip("/").split("/")) or "\\" in file.filename): raise ValueError("Invalid library archive layout") name = file.filename.rstrip("/").lower() if name in names: @@ -90,6 +91,8 @@ def publish_libraries(output, release=None): path = f"libraries/{platform}-{checksum}.zip" temporary.replace(output / path) manifest["platforms"][platform] = {"path": path, "size": size, "expandedBytes": expanded, "sha256": checksum} + except zipfile.BadZipFile as error: + raise ValueError(f"Invalid release archive {asset['name']}: {error}") from error finally: temporary.unlink(missing_ok=True) (output / "release.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") @@ -158,6 +161,8 @@ def publish_release(output): raise RuntimeError(f"Could not restore release output. Previous site is preserved at {previous}") from error raise published = True + except zipfile.BadZipFile as error: + raise ValueError(f"Invalid release archive {asset['name']}: {error}") from error finally: if published or not previous.exists(): shutil.rmtree(temporary) diff --git a/tools/check_compile.py b/tools/check_compile.py index c2c0befc3a..24daf7321a 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -11,8 +11,19 @@ from contextlib import nullcontext from pathlib import Path CPP = (".cpp", ".cc", ".cxx", ".c") -DROP = ("-c", "-MD", "-MMD", "-MP") +DROP = ("-c", "-MD", "-MMD", "-MP", "-fsyntax-only", "-fno-syntax-only", "-save-temps", "--save-temps", + "-save-temps=cwd", "-save-temps=obj", "--save-temps=cwd", "--save-temps=obj") DROP_WITH_VALUE = ("-o", "-MT", "-MF", "-MQ", "-MJ") +PREPROCESSING = ("-E", "--preprocess", "-M", "--dependencies", "-MM", "--user-dependencies", + "-MG", "--print-missing-file-dependencies", "-P", "--no-line-commands", + "-C", "-CC", "-dM", "-dD", "-dI") +WITH_VALUE = ("-I", "-L", "-F", "-B", "-D", "-U", "-x", "-include", "--include", "-imacros", "--imacros", + "--define-macro", "--undefine-macro", "-iquote", "-isystem", "-idirafter", "-iprefix", + "-iwithprefix", "-iwithprefixbefore", "-isysroot", "--sysroot", "-target", "--target", + "-arch", "-std", "-T", "-u", "-e", "--param", "-Xclang", "-Xpreprocessor", "-Xassembler", "-Xlinker") +WITHOUT_VALUE = (*DROP, *PREPROCESSING, "-S", "-fsyntax-only", "-Werror", "-Wall", "-Wextra", + "-pedantic", "-pedantic-errors", "-pthread", "-pipe", "-v") +JOINED_OPTIONS = ("-I", "-L", "-F", "-B", "-D", "-U", "-O", "-g", "-W") def capture(command, cwd=None, encoding="utf-8"): @@ -149,32 +160,88 @@ def syntax_only(entry, source): return syntax_arguments(arguments, directory, source) +def preprocessor_arguments(argument): + forwarded = argument[4:].split(",") + if any(option.startswith("@") for option in forwarded): + raise ValueError(f"unsupported forwarded compiler option response file in {argument}") + options = iter(forwarded) + kept = [] + for option in options: + if option in ("-MD", "-MMD", "-MF", "-MT", "-MQ", "--write-dependencies", "--write-user-dependencies"): + if not next(options, None): + raise ValueError(f"missing value for {option} in {argument}") + continue + if option in ("-M", "-MM", "-MG", "-MP", "--dependencies", "--user-dependencies", + "--print-missing-file-dependencies") or option.startswith(("-MF", "-MT", "-MQ")): + continue + kept.append(option) + if option in ("-D", "-U", "-I", "-F", "-include", "--include", "-imacros", "--imacros", + "-isystem", "-iquote", "-idirafter", "-isysroot"): + value = next(options, None) + if not value: + raise ValueError(f"missing operand for {option} in {argument}") + kept.append(value) + elif not (option.startswith(("-D", "-U", "-I", "-F")) and len(option) > 2): + raise ValueError(f"unsupported forwarded compiler option {option} in {argument}") + return "-Wp," + ",".join(kept) if kept else None + + def syntax_arguments(arguments, directory, source): validate_arguments(arguments) for argument in arguments[1:]: if argument.startswith("@"): raise ValueError(f"unexpanded response file {argument[1:]}; provide expanded compile arguments before checking syntax") wanted = source.resolve() - trimmed = [] - index = 0 + trimmed = [arguments[0]] + index = 1 + positional = False + syntax_added = False while index < len(arguments): argument = arguments[index] - if argument in DROP_WITH_VALUE: - index += 2 - continue - if argument in DROP or argument.startswith(("-MT", "-MF", "-MQ", "-MJ")): + if not syntax_added and argument.startswith("-"): + trimmed.append("-fsyntax-only") + syntax_added = True + if not positional and argument == "--": + positional = True + trimmed.append(argument) index += 1 continue - if not argument.startswith("-"): + if not positional and (argument in ("-Xclang", "-Xpreprocessor") or argument.startswith(("-Xclang=", "-Xpreprocessor="))): + raise ValueError(f"unsupported forwarded compiler option {argument}") + if not positional and argument in (*DROP_WITH_VALUE, *WITH_VALUE): + if index + 1 == len(arguments): + raise ValueError(f"missing operand for {argument}") + if argument in WITH_VALUE: + trimmed.extend(arguments[index:index + 2]) + index += 2 + continue + if not positional and argument.startswith("-Wp,"): + if forwarded := preprocessor_arguments(argument): + trimmed.append(forwarded) + index += 1 + continue + if not positional and (argument in (*DROP, *PREPROCESSING) or argument.startswith(("-MT", "-MF", "-MQ", "-MJ"))): + index += 1 + continue + if positional or not argument.startswith("-"): candidate = Path(argument) if not candidate.is_absolute(): candidate = directory / candidate if candidate.resolve() == wanted: index += 1 continue + if positional: + trimmed.append(str(candidate.resolve())) + index += 1 + continue + elif (argument not in WITHOUT_VALUE and not argument.startswith(JOINED_OPTIONS) and "=" not in argument and index + 1 < len(arguments) + and arguments[index + 1] in PREPROCESSING): + raise ValueError(f"unknown operand count for {argument} before {arguments[index + 1]}") trimmed.append(argument) index += 1 - trimmed += ["-fsyntax-only", str(wanted)] + if not syntax_added: + trimmed.append("-fsyntax-only") + trimmed.append(str(wanted)) return trimmed diff --git a/tools/check_conventions.py b/tools/check_conventions.py index fb378cda0d..b6b9a25b67 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -1,5 +1,6 @@ import argparse import io +import itertools import json import os import posixpath @@ -24,6 +25,7 @@ RULES = { "system-dependency": ("error", "CONTRIBUTING.md#code", "Third-party code is a submodule under 3rdparty/ built from source, not found on the system"), "title-specific": ("error", "CONTRIBUTING.md#code", "Implement the general behaviour, not what one title needs; title-specific code belongs in its sce_module"), "extension": ("error", "CONTRIBUTING.md#code", "Avoid non-standard extensions where standard C++ is enough"), + "alignment": ("error", "CONTRIBUTING.md#code", "MinGW GCC rejects static objects aligned above 8192 bytes; align a larger block at run time inside a bigger buffer"), "notes-file": ("error", "CONTRIBUTING.md#branches-and-pull-requests", "Notes, investigation and agent files go in the pull request, not in the repository; only the root AGENTS.md is allowed"), "binary": ("error", "docs/dev/CONVENTIONS.md", "Only UTF-8 text files in the repository; images go in the gist comments"), "ci-gate": ("error", "CONTRIBUTING.md#build-and-test", "A job that runs for pull requests needs a file filter: paths on the pull_request trigger, or needs eligibility and an if on its outputs"), @@ -59,6 +61,12 @@ STUB_BODY = re.compile(r"^(?:\(void\)\s*\w+\s*;|static_cast\(\s*\w+\s*\)\s SYSTEM_DEPENDENCY = re.compile(r"\b(find_package\s*\((?!\s*(Python3|Threads|Git)\b)|pkg_check_modules\b|pkg_search_module\b|find_(library|path|program|file)\b)", re.I) TITLE = re.compile(r"\b(PPSA|CUSA)\d{5}\b") EXTENSION = re.compile(r"__attribute__|__declspec|#\s*pragma\s+(?!once\b)") +INTEGER = r"(0[xX][0-9a-fA-F']+|0[bB][01']+|[0-9][0-9']*)[uUlLzZ]*" +ALIGNAS = re.compile(rf"\balignas\s*\(\s*(?:{INTEGER}|([A-Za-z_][A-Za-z0-9_]*))\s*\)") +CONSTANT = re.compile(rf"\bconstexpr\b[^;=]*?\b([A-Za-z_][A-Za-z0-9_]*)\s*(?:=\s*|\{{\s*){INTEGER}\s*[;}}]") +NAMESPACE = re.compile(r"\bnamespace\s+((?:[A-Za-z_][A-Za-z0-9_]*\s*::\s*(?:inline\s+)?)*[A-Za-z_][A-Za-z0-9_]*)\s*$|\bnamespace\s*$") +LINKAGE = re.compile(r'\bextern\s*""\s*$') +ALIGNMENT_LIMIT = 8192 LINK = re.compile(r"\[[^\]]+\]\(([^)\s]+)") FENCE = re.compile(r"^ {0,3}(```|~~~).*?^ {0,3}\1[ \t]*\r?$|^ {0,3}(?:```|~~~).*\Z", re.S | re.M) INLINE = re.compile(r"`[^`\n]*`") @@ -172,12 +180,20 @@ class Check: source = self.show(path).split("\n") masked = cpp_text("\n".join(source), preserve_comments=True).split("\n") assembly = asm_exports("\n".join(source)) if core and path.startswith("core/libs/prx/") else {} + constants, starts = None, None for number, text in lines: bare = masked[number - 1] if ("//" in bare or "/*" in bare) and not ALLOWED_COMMENT.match(bare): self.report("comment", path, number) if EXTENSION.search(bare): self.report("extension", path, number, EXTENSION.search(bare).group(0)) + if "alignas" in bare: + if constants is None: + constants = named_constants(cpp_text("\n".join(source))) + starts = list(itertools.accumulate((len(line) + 1 for line in masked), initial=0)) + alignment = over_aligned(bare, constants, starts[number - 1]) + if alignment: + self.report("alignment", path, number, alignment) if core and TITLE.search(text): self.report("title-specific", path, number, TITLE.search(text).group(0)) if core and path.startswith("core/libs/prx/"): @@ -385,8 +401,12 @@ class Check: body = re.sub(r"", "", body or "", flags=re.S) sections = dict(zip(SECTION.findall(body), SECTION.split(body)[2::2])) for name in ("What", "Tested", "Checklist"): - if not sections.get(name, "").strip(): - self.report("pr-template", "", 0, f"section '{name}' is missing or empty") + if name not in sections: + found = re.search(rf"^[ \t]*#+[ \t]*{name}[ \t]*$", body, re.M | re.I) + hint = f"; found '{found.group(0).strip()}'" if found else "" + self.report("pr-template", "", 0, f"add the '### {name}' heading of the template{hint}") + elif not sections[name].strip(): + self.report("pr-template", "", 0, f"fill the '### {name}' section") checklist = sections.get("Checklist", "") for item in re.findall(r"^\s*[-*+]\s*\[ \]\s*(.+)$", checklist, re.M): self.report("pr-template", "", 0, f"unchecked: {item.strip()}") @@ -464,13 +484,44 @@ def block(lines, start): return lines[start + 1:end] +def flow_mapping(value): + tokens = re.findall(r'''"(?:\\.|[^"\\])*"|'(?:''|[^'])*'|[^{}\[\],:\s]+|[{}\[\],:]''', value) + if not tokens or tokens[0] != "{" or tokens[-1] != "}": + return {} + result = {} + stack = [] + start, colon = 1, None + for index, token in enumerate(tokens): + if token in ("{", "["): + stack.append(token) + elif token in ("}", "]"): + if not stack or stack.pop() != ("{" if token == "}" else "["): + return {} + if not stack and index != len(tokens) - 1: + return {} + if len(stack) == 1 and token == ":" and colon is None: + colon = index + if (len(stack) == 1 and token == ",") or (not stack and token == "}"): + if colon == start + 1: + result[tokens[start].strip("'\"")] = " ".join(tokens[colon + 1:index]) + elif start != index: + return {} + start, colon = index + 1, None + return result if not stack else {} + + def pull_request_filtered(lines, trigger): _, _, _, value = trigger if value: + if value.lstrip().startswith("{"): + triggers = flow_mapping(value) + return "pull_request" not in triggers or bool({"paths", "paths-ignore"} & flow_mapping(triggers["pull_request"]).keys()) return "pull_request" not in re.findall(r"[A-Za-z_]+", value) triggers = block(lines, lines.index(trigger)) for index, (_, indent, name, value) in enumerate(triggers): if name == "pull_request" and indent == triggers[0][1]: + if value: + return bool({"paths", "paths-ignore"} & flow_mapping(value).keys()) return any(entry[2] in ("paths", "paths-ignore") for entry in block(triggers, index)) return True @@ -580,6 +631,68 @@ def cpp_text(text, preserve_comments=False): return CPP_TOKEN.sub(mask, text) +def integer_literal(text): + digits = text.replace("'", "").lower() + if digits.startswith(("0x", "0b")): + return int(digits, 0) + return int(digits, 8 if len(digits) > 1 and digits[0] == "0" else 10) + + +def brace_scopes(text): + stack, scopes, boundary = [], [], -1 + for index, character in enumerate(text): + if character not in "{};": + continue + if character == "{": + head = text[boundary + 1:index] + outer = next((key[1] for _, key in reversed(stack) if key and key[0] == "namespace"), "") + match = NAMESPACE.search(head) + if match and re.search(r"\binline\s+namespace\b", head): + key = ("namespace", outer) + elif match: + key = ("namespace", outer + "::" + re.sub(r"\binline\b|\s+", "", match.group(1) or "{anonymous}")) + else: + key = None if LINKAGE.search(head) else ("block", index) + stack.append((index, key)) + elif character == "}" and stack: + start, key = stack.pop() + scopes.append((start, index, key)) + boundary = index + return scopes + + +def scope_chain(scopes, position): + chain = [] + for _, _, key in sorted((scope for scope in scopes if scope[0] < position < scope[1]), reverse=True): + if key and key[0] == "namespace": + parts = key[1].split("::") + chain.extend(prefix for prefix in (("namespace", "::".join(parts[:count])) for count in range(len(parts), 1, -1)) if prefix not in chain) + elif key: + chain.append(key) + return chain + [("namespace", "")] + + +def named_constants(text): + scopes = brace_scopes(text) + definitions = [(match.group(1), integer_literal(match.group(2)), match.start(), scope_chain(scopes, match.start())[0]) for match in CONSTANT.finditer(text)] + return definitions, scopes + + +def constant_value(constants, name, position): + definitions, scopes = constants + chain = scope_chain(scopes, position) + visible = [(-chain.index(scope), start, value) for found, value, start, scope in definitions if found == name and start < position and scope in chain] + return max(visible)[2] if visible else 0 + + +def over_aligned(text, constants=((), ()), offset=0): + for match in ALIGNAS.finditer(text): + value = integer_literal(match.group(1)) if match.group(1) else constant_value(constants, match.group(2), offset + match.start()) + if value > ALIGNMENT_LIMIT: + return match.group(0) + return None + + def function_body(lines, start): text = cpp_text("\n".join(lines[start:start + 400])) brace, semicolon = text.find("{"), text.find(";") diff --git a/tools/check_main_sync.py b/tools/check_main_sync.py index f6d7532082..cc35cad442 100644 --- a/tools/check_main_sync.py +++ b/tools/check_main_sync.py @@ -4,7 +4,7 @@ import subprocess import sys from pathlib import Path -PULL_REQUEST = re.compile(r"Merge pull request #(\d+)") +PULL_REQUEST = re.compile(r"Merge pull request #(\d+)|\(#(\d+)\)$") BASES = ("upstream/main", "origin/main", "main") @@ -47,20 +47,22 @@ def merged_pull_requests(repo, mergebase, base, paths): for line in lines(repo, "-c", "core.quotepath=off", "log", "--first-parent", "--format=%h %s", f"{mergebase}..{base}", "--", *paths): match = PULL_REQUEST.search(line) if match: - found[int(match.group(1))] = line.split(" ", 1)[0] + found[int(match.group(1) or match.group(2))] = line.split(" ", 1)[0] return found def report(repo, base, head): repo = git(repo, "rev-parse", "--show-toplevel").stdout.strip() found = git(repo, "merge-base", base, head, check=False) - if found.returncode: + if found.returncode == 1: raise RuntimeError(f"{head} and {base} have no common ancestor") + if found.returncode: + raise RuntimeError(f"git merge-base failed: {found.stderr.strip()}") mergebase = found.stdout.strip() - behind = int(git(repo, "rev-list", "--count", f"{head}..{base}").stdout) - ahead = int(git(repo, "rev-list", "--count", f"{base}..{head}").stdout) - mine = set(names(repo, "diff", "-z", "--name-only", mergebase, head)) - theirs = set(names(repo, "diff", "-z", "--name-only", mergebase, base)) + behind = int(git(repo, "rev-list", "--count", f"{head}..{base}", "--").stdout) + ahead = int(git(repo, "rev-list", "--count", f"{base}..{head}", "--").stdout) + mine = set(names(repo, "diff", "-z", "--name-only", mergebase, head, "--")) + theirs = set(names(repo, "diff", "-z", "--name-only", mergebase, base, "--")) shared = sorted(mine & theirs) return { "base": base, diff --git a/tools/check_skips.py b/tools/check_skips.py index d29c91b065..936570e00e 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -11,26 +11,34 @@ NO_DISPLAY = re.compile(r"skipped,.*no display", re.IGNORECASE) NO_VULKAN_DEVICE = re.compile(r"skipped,.*no usable Vulkan device", re.IGNORECASE) HIGH_REFRESH_LIMITATION = re.compile(r"current display does not provide 120 Hz", re.IGNORECASE) +CTEST_SKIP_STATUS = re.compile(r"SKIP_RETURN_CODE=\d+|SKIP_REGULAR_EXPRESSION_MATCHED", re.IGNORECASE) WINDOWS_PREFIX = "agc" +def device_limitations(output, patterns): + reasons = [match.group(0) for line in output.splitlines() if (match := re.search(r"skipped,.*", line, re.IGNORECASE))] + return bool(reasons) and all(any(pattern.search(reason) for pattern in patterns) for reason in reasons) + + def high_refresh_limitation(output): reasons = [] for line in output.splitlines(): reason = line.strip() - if reason.lower().startswith("skipped,"): - reason = reason[len("skipped,"):].strip() + if match := re.search(r"skipped,", reason, re.IGNORECASE): + reason = reason[match.end():].strip() if reason: reasons.append(reason) return bool(reasons) and all(HIGH_REFRESH_LIMITATION.fullmatch(reason) for reason in reasons) -def skips(path): +def skips(path, require_tests=False): root = ET.parse(path).getroot() if root.tag not in ("testsuite", "testsuites"): raise ValueError(f"{path}: expected a JUnit testsuite or testsuites root, got {root.tag!r}") + found = False for case in root.iter("testcase"): + found = True if case.get("status") == "disabled": raise ValueError(f"{path}: testcase {case.get('name')!r} is disabled and was not run") skipped = case.find("skipped") @@ -39,9 +47,12 @@ def skips(path): name = case.get("name") if not name: raise ValueError(f"{path}: skipped testcase has no name") - output = ["skipped, " + reason.strip() for reason in (skipped.get("message", ""), skipped.text or "") if reason.strip()] + output = ["skipped, " + reason.strip() for reason in (skipped.get("message", ""), skipped.text or "") + if reason.strip() and not CTEST_SKIP_STATUS.fullmatch(reason.strip())] output += [case.findtext(channel) or "" for channel in ("system-out", "system-err")] yield name, "\n".join(output) + if require_tests and not found: + raise ValueError(f"{path}: report contains no testcases") def main(argv=None): @@ -52,21 +63,23 @@ def main(argv=None): parser.add_argument("junit", type=Path, help="the file written by ctest --output-junit") parser.add_argument("--job", choices=("linux", "windows"), default="linux") parser.add_argument("--list", action="store_true", help="print the skipped tests and why the file says they skipped") + parser.add_argument("--require-tests", action="store_true", help="fail if the report contains no testcases") args = parser.parse_args(argv) try: - found = sorted(skips(args.junit)) + found = sorted(skips(args.junit, require_tests=args.require_tests)) except (OSError, ET.ParseError, ValueError) as error: parser.error(str(error)) if args.list: for name, output in found: - reason = next((line.strip() for line in output.splitlines() if line.strip().lower().startswith("skipped")), "") + reason = next((match.group(0) for line in output.splitlines() + if (match := re.search(r"skipped,.*", line, re.IGNORECASE))), "") print(f"{name}: {reason}") return 0 if args.job == "windows": - unexpected = [name for name, output in found if not name.startswith(WINDOWS_PREFIX) and not NO_DISPLAY.search(output) and not NO_VULKAN_DEVICE.search(output) and not high_refresh_limitation(output)] + unexpected = [name for name, output in found if not name.startswith(WINDOWS_PREFIX) and not device_limitations(output, (NO_DISPLAY, NO_VULKAN_DEVICE)) and not high_refresh_limitation(output)] expected = f"any {WINDOWS_PREFIX}* test, which the runner has no Vulkan driver for, or a test that reports no display or Vulkan device or a display without a 120 Hz mode" else: - unexpected = [name for name, output in found if not DEVICE_LIMITATION.search(output) and not high_refresh_limitation(output)] + unexpected = [name for name, output in found if not device_limitations(output, (DEVICE_LIMITATION,)) and not high_refresh_limitation(output)] expected = "any test that reports the device limitation it needs, such as narrow subgroups, a missing extension, no display for a Vulkan window or a display without a 120 Hz mode" for name in unexpected: print(f"error: {name} skipped") diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index e74e2f72e6..518a885bd4 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -11,7 +11,7 @@ LEXICAL = re.compile(r'\\.|"(?:\\.|[^"\\])*"|\[(?P=*)\[.*?\](?P= r'|(?P\#\[(?P=*)\[.*?\](?P=comment_equals)\]|\#[^\n]*)', re.S) KNOWN_UNRUN = set() -SCRIPT = re.compile(r"(?:[\w.-]+/)*test_\w+\.py") +SCRIPT = re.compile(r"(?= len(sections) or sections[link][1] != 3: raise AuditError(f"{path}: invalid dynamic symbol table (entry size {entry}, size {size}, string table section {link})") + take(data, offset, size, path, "dynamic symbol table") strings_offset, strings_size = sections[link][4], sections[link][5] take(data, strings_offset, strings_size, path, "dynamic string table") for position in range(0, size, 24): @@ -212,6 +220,7 @@ def elf_imports(path, data): elif kind == 6: if entry != 16 or size % 16 or link >= len(sections) or sections[link][1] != 3: raise AuditError(f"{path}: invalid dynamic section (entry size {entry}, size {size}, string table section {link})") + take(data, offset, size, path, "dynamic section") strings_offset, strings_size = sections[link][4], sections[link][5] take(data, strings_offset, strings_size, path, "dynamic string table") terminated = False @@ -267,6 +276,8 @@ def pe_headers(path, data): def pe_exports(path, data): to_offset, directory, _ = pe_headers(path, data) rva, size = unpack(" BRANCH_VARIANT_LIMIT: + raise AuditError(f"more than {BRANCH_VARIANT_LIMIT} preprocessor branch combinations in one function") + return variants + + return expand(root) + + +def variant_calls(body): + return [unconditional_calls(variant) for variant in branch_variants(body)] + + +def unconditional_calls(body): + names = set() + for offset, statement in top_level_statements(body): + block = statement.strip() + if block.startswith("{") and block.endswith("}"): + names |= unconditional_calls(block) + elif not (CONDITIONAL.match(block) or BRANCHING.search(statement)): + for call in progress.CALL.finditer(statement): + before = statement[:call.start()] + if before.count("{") == before.count("}") and not progress.QUALIFIED.search(body, 0, offset + call.start()): + names.add(call.group(1)) + if EXIT.search(statement): + break + return names + + +def always_stub_calls(text): + helpers = {} + for match in progress.STUB_WRAPPER.finditer(text): + body = text[match.end() - 1:progress.body_end(text, match.end() - 1) + 1] + helpers.setdefault(match.group(1), []).extend(variant_calls(body)) + calls = {progress.STUB} + while True: + found = {name for name, bodies in helpers.items() if name not in calls and all(targets & calls for targets in bodies)} + if not found: + return calls + calls |= found + + +def conditional_stubs(library): + conditional = set() + for source in library.rglob("*.cpp"): + if "tests" in source.relative_to(library).parts: + continue + text = progress.code(source.read_text(errors="ignore")) + calls = always_stub_calls(text) + for match in progress.DEFINITION.finditer(text): + body = text[match.end() - 1:progress.body_end(text, match.end() - 1) + 1] + if not all(targets & calls for targets in variant_calls(body)): + conditional.add(match.group(1)) + return conditional + + def source_stubs(source): + return source_stub_classes(source)[0] + + +def source_stub_classes(source): if not Path(source).is_dir(): raise AuditError(f"{source}: not a directory") stubs = {} + partial = set() shared = {} for library in sorted(item for item in Path(source).iterdir() if item.is_dir()): aliases = {} for file in nid_names.source_files(library): - for nid, name in nid_names.source_aliases(file.read_text(errors="ignore")): + for nid, name in nid_names.source_aliases(file.read_text(encoding="utf-8-sig", errors="ignore")): aliases.setdefault(name, set()).add(nid) group = progress.scan_library(library) if "shared_sources" in group: shared[library.name] = group["shared_sources"] done = {nid for name in group["done_names"] for nid in nids_of(name, aliases)} + conditional = conditional_stubs(library) if group["todo_names"] else set() for name in group["todo_names"]: for nid in nids_of(name, aliases) - done: stubs.setdefault((library.name + ".prx", nid), name) + if name in conditional: + partial.add((library.name + ".prx", nid)) for library, owner in shared.items(): chain = [library] while owner in shared: @@ -498,7 +630,9 @@ def source_stubs(source): for (provider, nid), name in list(stubs.items()): if provider == owner + ".prx": stubs[(library + ".prx", nid)] = name - return stubs + if (provider, nid) in partial: + partial.add((library + ".prx", nid)) + return stubs, partial def read_registry(path): @@ -569,17 +703,17 @@ class ModuleNames(set): return isinstance(library, str) and module_stem(library, self.fold) in self.stems -def classify(nid, library, providers, stubs, modules): +def classify(nid, library, providers, stubs, modules, partial=frozenset()): if library and library in modules: return "module" if not providers: return "absent" if all((provider, nid) in stubs for provider in providers): - return "stub" + return "partial" if any((provider, nid) in partial for provider in providers) else "stub" return "implemented" -def audit(imports, exports, stubs, libraries, modules, module_imports=None, module_exports=None, *, needed_libraries=(), windows_libraries=()): +def audit(imports, exports, stubs, libraries, modules, module_imports=None, module_exports=None, *, needed_libraries=(), windows_libraries=(), partial=frozenset()): windows_names = {} for library in windows_libraries: windows_names.setdefault(library.casefold(), set()).add(library) @@ -622,11 +756,11 @@ def audit(imports, exports, stubs, libraries, modules, module_imports=None, modu 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) + kind = classify(resolved_nid, library, providers, stubs, modules, partial) record["class"] = kind record["providers"] = providers - record["library_mismatch"] = kind in ("implemented", "stub") and bool(library) and system_library(library) not in providers and "libc.prx" not in providers - record["name"] = next((stubs[(provider, resolved_nid)] for provider in providers if (provider, resolved_nid) in stubs), None) if kind == "stub" else None + record["library_mismatch"] = kind in ("implemented", "partial", "stub") and bool(library) and system_library(library) not in providers and "libc.prx" not in providers + record["name"] = next((stubs[(provider, resolved_nid)] for provider in providers if (provider, resolved_nid) in stubs), None) if kind in ("partial", "stub") else None ordered = sorted(records.values(), key=lambda record: (record["class"], record["library"] or "", record.get("module") or "", record["nid"])) check_conservation(len(imports) + sum(len(symbols) for symbols, _ in (module_imports or {}).values()), ordered) missing = defaultdict(int) @@ -747,7 +881,8 @@ def render(summary, records, missing, stops, missing_hosts=(), located=None): lines = [f"{summary['references']} references, {summary['unique_imports']} unique imports"] for kind in CLASSES: lines.append(f" {kind:<12}{summary['unique_by_class'][kind]:>6} imports {summary['references_by_class'][kind]:>6} references") - for title, kind in (("Absent: no built library exports these", "absent"), ("Stub: exported but throws when called", "stub")): + for title, kind in (("Absent: no built library exports these", "absent"), ("Stub: exported but throws when called", "stub"), + ("Partial: exported, throws only for inputs whose behaviour is unknown", "partial")): selected = [record for record in records if record["class"] == kind] if selected: lines.append("") @@ -787,6 +922,10 @@ def render(summary, records, missing, stops, missing_hosts=(), located=None): def main(argv=None): + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace") + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="classify the imports of a converted game against the built system libraries") parser.add_argument("registries", nargs="+", type=Path, help="JSON written by 'relinker --registry'") parser.add_argument("--libs", action="append", required=True, help="directory of built .prx files; repeat for several") @@ -819,7 +958,8 @@ def main(argv=None): modules = set(module_imports) modules |= {name.removesuffix(".guest.prx") for name in modules if name.endswith(".guest.prx")} modules = ModuleNames(modules, fold=bool(windows)) - records, missing = audit(imports, exports, source_stubs(args.source), libraries, modules, module_imports, module_exports, needed_libraries=needed, windows_libraries=windows) + stubs, partial = source_stub_classes(args.source) + records, missing = audit(imports, exports, stubs, libraries, modules, module_imports, module_exports, needed_libraries=needed, windows_libraries=windows, partial=partial) missing_hosts = audit_host_dependencies(host_dependencies, libraries, modules, windows) if args.names: attach_names(records, load_names(args.names)) @@ -845,7 +985,4 @@ def main(argv=None): if __name__ == "__main__": - for stream in (sys.stdout, sys.stderr): - if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8", errors="replace") raise SystemExit(main()) diff --git a/tools/input-mapper/app.js b/tools/input-mapper/app.js new file mode 100644 index 0000000000..4703578fe0 --- /dev/null +++ b/tools/input-mapper/app.js @@ -0,0 +1,446 @@ +import {actions, defaults, groups, mouseValues, typesForAction, wheelValues} from './mapping.js'; +import {exportableKeys} from './keys.js'; +import {captureKey} from './capture.js'; +import {parseIni, serialize, validate} from './validation.js'; +import {diagnostic, initialLocale, locales, translate} from './i18n.js'; +import {createDropdown} from './dropdown.js'; + +const byId = id => document.getElementById(id); +let entries = []; +let savedConfiguration = '[]'; +let captureTarget; +let captureButton; +let selectedGroup = groups[0].name; +let locale = initialLocale(); +let languageControl; +const bindingControls = new WeakMap(); +let activeView = ['controls', 'configuration', 'help'].includes(location.hash.slice(1)) ? location.hash.slice(1) : 'controls'; +let currentStatus; +let importReport; +const dialog = byId('capture-dialog'); +try { + const savedLocale = localStorage.getItem('anyps5-controls-language'); + locale = initialLocale(savedLocale); +} catch {} +const t = (key, values) => translate(locale, key, values); +const groupLabel = name => t(`group${groups.findIndex(group => group.name === name)}`); + +function setStatus(key, values = {}) { + currentStatus = {key, values}; + byId('status').textContent = t(key, values); +} + +function showView(view, focus = true, updateHash = true) { + activeView = view; + const controls = view === 'controls'; + byId('categories').hidden = !controls; + byId('toolbar').hidden = !controls; + byId('mappings').hidden = !controls; + byId('configuration').hidden = view !== 'configuration'; + byId('help').hidden = view !== 'help'; + byId('validation').hidden = view === 'help'; + byId('status').hidden = view === 'help'; + byId('import-report').hidden = view === 'help' || !importReport; + byId('view-intro').textContent = t(controls ? 'intro' : view === 'help' ? 'helpIntro' : 'configIntro'); + if (controls) filterActions(); + else byId('group-title').textContent = t(view); + for (const link of document.querySelectorAll('[data-view]')) { + const current = link.dataset.view === view; + link.classList.toggle('current', current); + if (current) link.setAttribute('aria-current', 'page'); + else link.removeAttribute('aria-current'); + } + if (updateHash && location.hash !== `#${view}`) history.pushState(null, '', `#${view}`); + if (focus) { + byId('content').focus({preventScroll: true}); + window.scrollTo({top: 0, behavior: 'auto'}); + } +} + +function renderHelp() { + const opened = [...byId('help').querySelectorAll('details')].map(item => item.open); + byId('help').replaceChildren(); + for (let index = 0; index < 4; index++) { + const question = element('details', undefined, 'faq'); + question.open = opened[index] || false; + question.append(element('summary', t(`faq${index}Question`)), element('p', t(`faq${index}Answer`))); + byId('help').append(question); + } +} + +function renderImportReport() { + if (!importReport) return; + const report = byId('import-report'); + report.replaceChildren(element('h2', t(importReport.errors.length ? 'importRejected' : 'importRead'))); + const messages = [...importReport.errors, ...importReport.warnings]; + if (messages.length) { + const list = element('ul'); + for (const item of messages) list.append(element('li', t('line', {line: item.line, message: diagnostic(locale, item.message)}))); + report.append(list); + } + if (importReport.errors.length) report.append(element('p', t('preserved'), 'error')); + report.hidden = false; +} + +function applyLocale() { + document.documentElement.lang = locale; + document.documentElement.dir = locales.find(item => item.code === locale).direction; + for (const node of document.querySelectorAll('[data-i18n]')) node.textContent = t(node.dataset.i18n); + for (const node of document.querySelectorAll('[data-i18n-label]')) node.setAttribute('aria-label', t(node.dataset.i18nLabel)); + for (const node of document.querySelectorAll('[data-i18n-placeholder]')) node.placeholder = t(node.dataset.i18nPlaceholder); + languageControl.label = t('language'); + languageControl.direction = document.documentElement.dir; + languageControl.text = dropdownText(); + languageControl.setValue(locale); + byId('settings').title = t('language'); + byId('categories').replaceChildren(); + renderCategories(); + renderHelp(); + render(); + renderImportReport(); + if (currentStatus) setStatus(currentStatus.key, currentStatus.values); + showView(activeView, false, false); + updateClock(); +} + +function element(tag, text, className) { + const node = document.createElement(tag); + if (text !== undefined) node.textContent = text; + if (className) node.className = className; + return node; +} + +function isDirty() { + return JSON.stringify(entries) !== savedConfiguration; +} + +async function confirmReplacement() { + if (!isDirty()) return true; + const confirmation = byId('replace-dialog'); + confirmation.returnValue = 'cancel'; + return new Promise(resolve => { + confirmation.addEventListener('close', () => resolve(confirmation.returnValue === 'discard'), {once: true}); + confirmation.showModal(); + }); +} + +function valueChoices(type) { + return type === 'KEY' ? exportableKeys : type === 'MOUSE' ? mouseValues : type === 'WHEEL' ? wheelValues : []; +} + +function dropdownText() { + return {search: t('searchOptions'), close: t('close'), empty: t('noOptions')}; +} + +function selectControl(label, choices, value) { + return createDropdown({label, choices: [{value: '', label: t('select')}, ...choices], value, placeholder: t('select'), text: dropdownText()}); +} + +function button(text, label, click) { + const control = element('button', text); + control.type = 'button'; + control.setAttribute('aria-label', label); + control.addEventListener('click', click); + return control; +} + +function renderCategories() { + const icons = ['face-icon', 'shoulder-icon', 'direction-icon', 'stick-icon', 'stick-icon', 'touch-icon']; + const symbols = ['△ ○ × □', 'L2 R2', '', 'L', 'R', '']; + groups.forEach((group, index) => { + const category = button('', groupLabel(group.name), () => { + selectedGroup = group.name; + byId('search').value = ''; + filterActions(); + }); + category.className = 'category'; + category.dataset.group = group.name; + const icon = element('span', undefined, 'category-icon'); + icon.setAttribute('aria-hidden', 'true'); + const mark = element('span', undefined, icons[index]); + if (index === 0) { + for (const symbol of symbols[index].split(' ')) mark.append(element('span', symbol)); + } else mark.textContent = symbols[index]; + icon.append(mark); + category.append(icon, element('span', t(`group${index}`), 'category-label')); + category.addEventListener('keydown', event => { + const direction = document.documentElement.dir === 'rtl' ? -1 : 1; + const offset = event.key === 'ArrowRight' ? direction : event.key === 'ArrowLeft' ? -direction : 0; + if (!offset && event.key !== 'Home' && event.key !== 'End') return; + event.preventDefault(); + const next = event.key === 'Home' ? 0 : event.key === 'End' ? groups.length - 1 : (index + offset + groups.length) % groups.length; + const target = byId('categories').children[next]; + target.click(); + target.focus(); + target.scrollIntoView({block: 'nearest', inline: 'nearest'}); + }); + byId('categories').append(category); + }); +} + +function renderBinding(entry, position) { + const row = element('div', undefined, 'binding'); + const label = t('bindingLabel', {action: entry.action, number: position + 1}); + const type = selectControl(t('typeLabel', {binding: label}), typesForAction(entry.action), entry.type); + const value = selectControl(t('valueLabel', {binding: label}), valueChoices(entry.type), entry.value); + type.element.dir = 'ltr'; + value.element.dir = 'ltr'; + const capture = button(t('capture'), t('captureLabel', {binding: label}), () => { + captureTarget = entry; + captureButton = capture; + byId('capture-title').textContent = t('captureTitle', {action: entry.action}); + byId('capture-status').textContent = ''; + dialog.showModal(); + }); + capture.disabled = entry.type !== 'KEY'; + const remove = button(t('remove'), t('removeLabel', {binding: label}), () => { + entries = entries.filter(binding => binding !== entry); + render(); + const remaining = document.querySelector(`[data-action="${entry.action}"] .dropdown-trigger`); + (remaining || document.querySelector(`[data-action="${entry.action}"] .add-binding`)).focus(); + }); + type.select.addEventListener('change', () => { + entry.type = type.select.value; + entry.value = ''; + value.setChoices([{value: '', label: t('select')}, ...valueChoices(entry.type)]); + value.setValue(''); + capture.disabled = entry.type !== 'KEY'; + updateOutput(); + }); + value.select.addEventListener('change', () => { + entry.value = value.select.value; + updateOutput(); + }); + row.append(type.element, value.element, capture, remove); + bindingControls.set(entry, value); + return row; +} + +function render() { + const container = byId('mappings'); + container.replaceChildren(); + for (const group of groups) { + const section = element('section', undefined, 'group'); + section.dataset.group = group.name; + section.append(element('h2', groupLabel(group.name))); + for (const action of group.actions) { + const article = element('div', undefined, 'action'); + article.dataset.action = action; + const heading = element('div', undefined, 'action-heading'); + const add = button(t('add'), t('addLabel', {action}), () => { + entries.push({action, type: 'KEY', value: ''}); + render(); + const rows = document.querySelectorAll(`[data-action="${action}"] .binding`); + rows[rows.length - 1].querySelectorAll('.dropdown-trigger')[1].focus(); + }); + add.className = 'add-binding'; + const title = element('div', undefined, 'action-title'); + const symbols = {Cross: '×', Circle: '○', Triangle: '△', Square: '□', Up: '↑', Right: '→', Down: '↓', Left: '←'}; + if (symbols[action]) { + const symbol = element('span', symbols[action], 'action-symbol'); + symbol.setAttribute('aria-hidden', 'true'); + title.append(symbol); + } + const actionName = element('h3', action); + actionName.dir = 'ltr'; + title.append(actionName); + heading.append(title, add); + article.append(heading); + const configured = entries.filter(entry => entry.action === action); + article.append(element('p', configured.length ? t('custom') : t('builtin', {inputs: defaults[action].join(', ') || t('noDefault')}), 'hint')); + configured.forEach((entry, position) => article.append(renderBinding(entry, position))); + section.append(article); + } + container.append(section); + } + filterActions(); + updateOutput(); +} + +function filterActions() { + const search = byId('search').value.trim().toLowerCase(); + if (activeView === 'controls') byId('group-title').textContent = search ? t('search') : groupLabel(selectedGroup); + for (const category of byId('categories').children) { + category.setAttribute('aria-pressed', String(!search && category.dataset.group === selectedGroup)); + } + let visible = 0; + for (const section of document.querySelectorAll('.group')) { + let matches = 0; + for (const action of section.querySelectorAll('.action')) { + action.hidden = search ? !`${section.dataset.group} ${groupLabel(section.dataset.group)} ${action.dataset.action}`.toLowerCase().includes(search) : section.dataset.group !== selectedGroup; + if (!action.hidden) matches++; + } + section.hidden = matches === 0; + visible += matches; + } + let empty = byId('no-results'); + if (!empty) { + empty = element('p', t('noResults'), 'hint'); + empty.id = 'no-results'; + byId('mappings').append(empty); + } + empty.hidden = visible !== 0; +} + +function updateOutput() { + const result = validate(entries); + const valid = result.errors.length === 0; + byId('preview').value = valid ? serialize(entries) : ''; + byId('count').textContent = `${new Set(result.bindings.map(binding => binding.action)).size} / ${actions.length}`; + byId('download').disabled = !valid; + byId('copy').disabled = !valid; + const report = byId('validation'); + report.replaceChildren(); + for (const [name, messages] of [['error', result.errors], ['warning', result.warnings]]) { + if (!messages.length) continue; + const list = element('ul', undefined, name); + for (const item of messages) list.append(element('li', diagnostic(locale, item.message))); + report.append(list); + } + const rows = [...document.querySelectorAll('.binding')]; + const renderedEntries = groups.flatMap(group => group.actions.flatMap(action => entries.filter(entry => entry.action === action))); + rows.forEach((row, index) => { + const invalid = result.errors.some(error => entries[error.index] === renderedEntries[index]); + for (const control of row.querySelectorAll('.dropdown-trigger')) control.setAttribute('aria-invalid', String(invalid)); + }); +} + +function cancelCapture(message) { + captureTarget = undefined; + dialog.close(); + captureButton?.focus(); + if (message) setStatus(message); +} + +byId('cancel-capture').addEventListener('click', () => cancelCapture('captureCancelled')); +dialog.addEventListener('cancel', event => { + event.preventDefault(); + cancelCapture('captureCancelled'); +}); +window.addEventListener('blur', () => { + if (captureTarget) cancelCapture('captureBlur'); +}); +window.addEventListener('keydown', event => { + if (!captureTarget) return; + event.preventDefault(); + event.stopImmediatePropagation(); + if (event.code === 'Escape' || event.key === 'Escape') { + cancelCapture('captureCancelled'); + return; + } + if (event.repeat) return; + const result = captureKey(event); + if (result.error) { + byId('capture-status').textContent = diagnostic(locale, result.error); + return; + } + const target = captureTarget; + target.value = result.value; + cancelCapture(); + bindingControls.get(target)?.setValue(result.value); + updateOutput(); + setStatus('captured', {value: result.value, action: target.action}); +}, true); + +byId('search').addEventListener('input', filterActions); +byId('reset').addEventListener('click', async () => { + if (!await confirmReplacement()) return; + entries = []; + savedConfiguration = '[]'; + importReport = undefined; + byId('import-report').hidden = true; + render(); + setStatus('resetDone'); +}); +byId('import').addEventListener('click', () => byId('file').click()); +byId('file').addEventListener('change', async event => { + const file = event.target.files[0]; + if (!file) return; + event.target.value = ''; + try { + if (file.size > 1024 * 1024) throw new Error(t('fileTooLarge')); + const text = new TextDecoder('utf-8', {fatal: true}).decode(await file.arrayBuffer()); + const result = parseIni(text); + const report = byId('import-report'); + importReport = result; + renderImportReport(); + if (result.errors.length) { + report.focus(); + return; + } + if (!await confirmReplacement()) return; + entries = result.entries; + savedConfiguration = JSON.stringify(entries); + render(); + setStatus('imported', {name: file.name}); + } catch (error) { + setStatus('importFailed', {message: error instanceof TypeError ? t('invalidUtf8') : error.message}); + } +}); +byId('download').addEventListener('click', () => { + const content = serialize(entries); + const url = URL.createObjectURL(new Blob([content], {type: 'text/plain;charset=utf-8'})); + const link = element('a'); + link.href = url; + link.download = 'anyps5-input.ini'; + document.body.append(link); + link.click(); + link.remove(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + savedConfiguration = JSON.stringify(entries); + setStatus('downloaded'); +}); +byId('copy').addEventListener('click', async () => { + try { + const content = serialize(entries); + const copiedConfiguration = JSON.stringify(entries); + await navigator.clipboard.writeText(content); + savedConfiguration = copiedConfiguration; + setStatus('copied'); + } catch { + byId('preview').focus(); + byId('preview').select(); + setStatus('clipboardFailed'); + } +}); +window.addEventListener('beforeunload', event => { + if (!isDirty()) return; + event.preventDefault(); + event.returnValue = ''; +}); +byId('focus-search').addEventListener('click', () => { + showView('controls'); + byId('search').focus(); +}); +for (const link of document.querySelectorAll('[data-view]')) { + link.addEventListener('click', event => { + event.preventDefault(); + showView(link.dataset.view); + }); +} +byId('back-controls').addEventListener('click', () => showView('controls')); +function restoreView() { + const view = location.hash.slice(1); + if (['controls', 'configuration', 'help'].includes(view)) showView(view, false, false); + else if (!view) showView('controls', false, false); +} +window.addEventListener('popstate', restoreView); +window.addEventListener('hashchange', restoreView); +languageControl = createDropdown({ + label: t('language'), choices: locales.map(item => ({value: item.code, label: item.name, lang: item.code, direction: item.direction})), + value: locale, placeholder: t('language'), text: dropdownText(), trigger: byId('settings'), direction: locales.find(item => item.code === locale).direction +}); +byId('language-control').append(languageControl.element); +languageControl.select.addEventListener('change', event => { + locale = event.target.value; + try { localStorage.setItem('anyps5-controls-language', locale); } catch {} + applyLocale(); +}); +function updateClock() { + const now = new Date(); + byId('clock').textContent = new Intl.DateTimeFormat(locale, {hour: '2-digit', minute: '2-digit'}).format(now); + byId('clock').dateTime = now.toISOString(); +} +updateClock(); +setInterval(updateClock, 60000); +applyLocale(); diff --git a/tools/input-mapper/capture.js b/tools/input-mapper/capture.js new file mode 100644 index 0000000000..05bbce0c36 --- /dev/null +++ b/tools/input-mapper/capture.js @@ -0,0 +1,30 @@ +import {exportableKeys} from './keys.js'; + +const codes = { + Enter: 'Return', Escape: 'Escape', Space: 'Space', Backspace: 'Backspace', Tab: 'Tab', + Minus: '-', Equal: '=', BracketLeft: '[', BracketRight: ']', Backslash: '\\', + Semicolon: ';', Quote: "'", Backquote: '`', Comma: ',', Period: '.', Slash: '/', + CapsLock: 'CapsLock', PrintScreen: 'PrintScreen', ScrollLock: 'ScrollLock', Pause: 'Pause', + Insert: 'Insert', Home: 'Home', PageUp: 'PageUp', Delete: 'Delete', End: 'End', PageDown: 'PageDown', + ArrowRight: 'Right', ArrowLeft: 'Left', ArrowDown: 'Down', ArrowUp: 'Up', NumLock: 'Numlock', + NumpadDivide: 'Keypad /', NumpadMultiply: 'Keypad *', NumpadSubtract: 'Keypad -', + NumpadAdd: 'Keypad +', NumpadEnter: 'Keypad Enter', NumpadDecimal: 'Keypad .', + NumpadEqual: 'Keypad =', NumpadComma: 'Keypad ,', ContextMenu: 'Application', + ControlLeft: 'Left Ctrl', ControlRight: 'Right Ctrl', ShiftLeft: 'Left Shift', ShiftRight: 'Right Shift', + AltLeft: 'Left Alt', AltRight: 'Right Alt', MetaLeft: 'Left GUI', MetaRight: 'Right GUI' +}; + +export function captureKey(event) { + if (event.isComposing || event.key === 'Dead' || event.key === 'Process') { + return {error: 'Composition and dead keys are ambiguous. Select an SDL key from the list.'}; + } + let value = codes[event.code]; + if (/^Key[A-Z]$/u.test(event.code)) value = event.code.slice(3); + else if (/^Digit[0-9]$/u.test(event.code)) value = event.code.slice(5); + else if (/^Numpad[0-9]$/u.test(event.code)) value = `Keypad ${event.code.slice(6)}`; + else if (/^F(?:[1-9]|1[0-9]|2[0-4])$/u.test(event.code)) value = event.code; + if (!value || !exportableKeys.includes(value)) { + return {error: 'This physical key cannot be captured in the current INI format. Select an SDL key from the list.'}; + } + return {value}; +} diff --git a/tools/input-mapper/dropdown.js b/tools/input-mapper/dropdown.js new file mode 100644 index 0000000000..63a962e2ea --- /dev/null +++ b/tools/input-mapper/dropdown.js @@ -0,0 +1,167 @@ +const dialog = document.getElementById('picker-dialog'); +const title = document.getElementById('picker-title'); +const search = document.getElementById('picker-search'); +const list = document.getElementById('picker-options'); +const empty = document.getElementById('picker-empty'); +const close = document.getElementById('picker-close'); +let active; +let visible = []; +let focused = 0; + +function renderOptions() { + const query = search.value.trim().toLocaleLowerCase(); + visible = active.choices.filter(item => `${item.label} ${item.value}`.toLocaleLowerCase().includes(query)); + list.replaceChildren(); + const exactMatch = query ? visible.findIndex(item => item.label.toLocaleLowerCase() === query || item.value.toLocaleLowerCase() === query) : -1; + focused = Math.max(0, exactMatch >= 0 ? exactMatch : visible.findIndex(item => item.value === active.select.value)); + visible.forEach((item, index) => { + const option = document.createElement('button'); + option.type = 'button'; + option.className = 'picker-option'; + option.setAttribute('role', 'option'); + option.setAttribute('aria-selected', String(item.value === active.select.value)); + option.tabIndex = index === focused ? 0 : -1; + option.textContent = item.label; + if (item.lang) option.lang = item.lang; + option.dir = item.direction || active.direction; + option.addEventListener('click', () => choose(item)); + option.addEventListener('focus', () => { focused = index; }); + list.append(option); + }); + empty.hidden = visible.length !== 0; + if (dialog.open) list.children[focused]?.scrollIntoView({block: 'nearest'}); +} + +function choose(item) { + const current = active; + current.setValue(item.value); + dialog.close(); + current.select.dispatchEvent(new Event('change', {bubbles: true})); +} + +function focusOption(index) { + if (!visible.length) return; + focused = (index + visible.length) % visible.length; + [...list.children].forEach((option, position) => { option.tabIndex = position === focused ? 0 : -1; }); + list.children[focused].focus({preventScroll: true}); + list.children[focused].scrollIntoView({block: 'nearest'}); +} + +function positionPicker() { + if (!dialog.open || !active) return; + if (matchMedia('(max-width: 600px)').matches) { + for (const name of ['top', 'left', 'width']) dialog.style.removeProperty(name); + return; + } + const rect = active.trigger.getBoundingClientRect(); + const width = Math.min(Math.max(rect.width, 320), innerWidth - 32); + dialog.style.width = `${width}px`; + const height = dialog.getBoundingClientRect().height; + const preferred = rect.bottom + 10; + const top = preferred + height <= innerHeight - 16 ? preferred : Math.max(16, rect.top - height - 10); + const left = active.direction === 'rtl' ? rect.right - width : rect.left; + dialog.style.top = `${Math.min(top, Math.max(16, innerHeight - height - 16))}px`; + dialog.style.left = `${Math.max(16, Math.min(left, innerWidth - width - 16))}px`; +} + +function openPicker(control) { + active = control; + title.textContent = control.label; + list.setAttribute('aria-label', control.label); + search.value = ''; + search.placeholder = control.text.search; + search.setAttribute('aria-label', control.text.search); + close.textContent = control.text.close; + empty.textContent = control.text.empty; + dialog.style.setProperty('--picker-height', `${Math.min(480, 136 + Math.min(control.choices.length, 8) * 46)}px`); + renderOptions(); + control.trigger.setAttribute('aria-expanded', 'true'); + dialog.showModal(); + positionPicker(); + list.children[focused]?.scrollIntoView({block: 'nearest'}); + search.focus(); +} + +search.addEventListener('input', renderOptions); +dialog.addEventListener('keydown', event => { + if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { + event.preventDefault(); + const inSearch = event.target === search; + focusOption(inSearch ? focused : focused + (event.key === 'ArrowDown' ? 1 : -1)); + } else if (event.target !== search && (event.key === 'Home' || event.key === 'End')) { + event.preventDefault(); + focusOption(event.key === 'Home' ? 0 : visible.length - 1); + } else if (event.target === search && event.key === 'Enter' && visible.length) { + event.preventDefault(); + choose(visible[focused]); + } +}); +close.addEventListener('click', () => dialog.close()); +dialog.addEventListener('click', event => { + if (event.target !== dialog) return; + const rect = dialog.getBoundingClientRect(); + if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) dialog.close(); +}); +dialog.addEventListener('close', () => { + const current = active; + active = undefined; + current?.trigger.setAttribute('aria-expanded', 'false'); + if (current?.trigger.isConnected) current.trigger.focus({preventScroll: true}); +}); +window.addEventListener('resize', positionPicker); + +export function createDropdown({label, choices, value, placeholder, text, trigger, direction = 'ltr'}) { + const wrapper = document.createElement('div'); + wrapper.className = 'dropdown'; + const select = document.createElement('select'); + select.hidden = true; + const providedTrigger = Boolean(trigger); + const control = trigger || document.createElement('button'); + control.type = 'button'; + if (!providedTrigger) { + control.className = 'dropdown-trigger'; + control.setAttribute('role', 'combobox'); + } + control.setAttribute('aria-label', label); + control.setAttribute('aria-haspopup', 'dialog'); + control.setAttribute('aria-controls', 'picker-dialog'); + control.setAttribute('aria-expanded', 'false'); + const selected = document.createElement('span'); + selected.className = 'dropdown-value'; + if (!providedTrigger) control.append(selected); + const api = { + element: wrapper, select, trigger: control, label, direction, text, choices: [], + setValue(next) { + select.value = next; + const choice = api.choices.find(item => item.value === select.value); + if (!providedTrigger) selected.textContent = choice?.label || placeholder; + if (!providedTrigger) control.title = choice?.label || placeholder; + }, + setChoices(next) { + const previous = select.value; + api.choices = next.map(item => typeof item === 'string' ? {value: item, label: item} : item); + select.replaceChildren(); + for (const item of api.choices) { + const option = document.createElement('option'); + option.value = item.value; + option.textContent = item.label; + select.append(option); + } + api.setValue(previous); + }, + focus() { control.focus(); } + }; + api.setChoices(choices); + api.setValue(value); + select.addEventListener('change', () => api.setValue(select.value)); + control.addEventListener('click', () => openPicker(api)); + control.addEventListener('keydown', event => { + if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { + event.preventDefault(); + openPicker(api); + } + }); + wrapper.append(select); + if (!providedTrigger) wrapper.append(control); + return api; +} diff --git a/tools/input-mapper/i18n.js b/tools/input-mapper/i18n.js new file mode 100644 index 0000000000..3f6b3f0d86 --- /dev/null +++ b/tools/input-mapper/i18n.js @@ -0,0 +1,1263 @@ +export const locales = [ + { + "code": "ar-EG", + "name": "العربية (المصري)", + "direction": "rtl" + }, + { + "code": "en", + "name": "English", + "direction": "ltr" + }, + { + "code": "es", + "name": "Español", + "direction": "ltr" + }, + { + "code": "fr", + "name": "Français", + "direction": "ltr" + }, + { + "code": "de", + "name": "Deutsch", + "direction": "ltr" + }, + { + "code": "it", + "name": "Italiano", + "direction": "ltr" + }, + { + "code": "pt", + "name": "Português", + "direction": "ltr" + }, + { + "code": "ru", + "name": "Русский", + "direction": "ltr" + }, + { + "code": "tr", + "name": "Türkçe", + "direction": "ltr" + }, + { + "code": "zh-Hans", + "name": "简体中文", + "direction": "ltr" + }, + { + "code": "ja", + "name": "日本語", + "direction": "ltr" + }, + { + "code": "ko", + "name": "한국어", + "direction": "ltr" + }, + { + "code": "hi", + "name": "हिन्दी", + "direction": "ltr" + }, + { + "code": "bn", + "name": "বাংলা", + "direction": "ltr" + }, + { + "code": "ur", + "name": "اردو", + "direction": "rtl" + }, + { + "code": "fa", + "name": "فارسی", + "direction": "rtl" + }, + { + "code": "id", + "name": "Bahasa Indonesia", + "direction": "ltr" + }, + { + "code": "vi", + "name": "Tiếng Việt", + "direction": "ltr" + }, + { + "code": "th", + "name": "ไทย", + "direction": "ltr" + }, + { + "code": "sw", + "name": "Kiswahili", + "direction": "ltr" + }, + { + "code": "nl", + "name": "Nederlands", + "direction": "ltr" + }, + { + "code": "pl", + "name": "Polski", + "direction": "ltr" + } +]; + +export const messages = { + "en": { + "controls": "Controls", + "configuration": "Configuration", + "help": "Help", + "settings": "Settings", + "language": "Language", + "close": "Close", + "eyebrow": "Accessories / Custom controls", + "intro": "Choose a control, then assign your keyboard or mouse inputs.", + "search": "Search controls", + "import": "Import INI", + "reset": "Reset", + "group0": "Face buttons", + "group1": "Shoulders", + "group2": "D-pad", + "group3": "Left stick", + "group4": "Right stick", + "group5": "Touch & utility", + "select": "Select…", + "add": "+ Add binding", + "capture": "Capture", + "remove": "Remove", + "builtin": "Built-in: {inputs}", + "custom": "Custom inputs replace built-in inputs.", + "noResults": "No controls match your search.", + "download": "Download INI", + "copy": "Copy configuration", + "previewEmpty": "Add a binding to begin.", + "backControls": "Back to controls", + "switchGroup": "Switch group", + "moveFocus": "Move focus", + "helpIntro": "A few answers to help you get started.", + "configIntro": "Review your inputs, then copy or download your file.", + "captureTitle": "Press a key for {action}", + "cancelCapture": "Cancel capture", + "keepEditing": "Keep editing", + "discard": "Discard changes", + "faq0Question": "How do I assign a key?", + "faq0Answer": "Choose a group, select Add binding, then Capture or choose a key from the list. Multiple bindings are alternatives.", + "faq1Question": "Where does my INI file go?", + "faq1Answer": "Download anyps5-input.ini and put it next to the generated game executable. For another location, set ANYPS5_INPUT_CONFIG to the file path.", + "faq2Question": "Why does a key not get captured?", + "faq2Answer": "Browser or system shortcuts may intercept it. Choose it manually from the list. Escape cancels capture; bind Escape using the list.", + "faq3Question": "Are my edits saved automatically?", + "faq3Answer": "Input edits stay in memory: copy or download before reloading. Only your language choice is saved in this browser. Unconfigured actions keep their defaults.", + "skip": "Skip to content", + "mainNav": "Main navigation", + "groups": "Control groups", + "find": "Find an action", + "clock": "Local time", + "output": "Configuration output", + "captureHelp": "Capture uses physical key positions. Escape cancels capture; choose Escape from the list to assign it. Some browser and system shortcuts require manual selection.", + "replaceTitle": "Discard unsaved changes?", + "replaceHelp": "Copy or download your configuration first if you want to keep it.", + "captureCancelled": "Capture cancelled.", + "captureBlur": "Capture cancelled because the window lost focus.", + "captured": "Captured {value} for {action}.", + "resetDone": "Configuration reset.", + "importRejected": "Import rejected", + "importRead": "Import read successfully", + "line": "Line {line}: {message}", + "preserved": "Your current configuration has been preserved.", + "imported": "Imported {name}.", + "importFailed": "Import failed: {message}", + "fileTooLarge": "INI file must be smaller than 1 MiB.", + "invalidUtf8": "The file must be valid UTF-8 text.", + "downloaded": "Configuration download requested.", + "copied": "Configuration copied.", + "clipboardFailed": "Clipboard unavailable. Copy the selected preview with Ctrl+C.", + "noScript": "Enable JavaScript to use this configurator.", + "noDefault": "no keyboard or mouse input", + "bindingLabel": "{action} binding {number}", + "typeLabel": "{binding} input type", + "valueLabel": "{binding} input value", + "addLabel": "Add binding to {action}", + "captureLabel": "Capture key for {binding}", + "removeLabel": "Remove {binding}", + "searchOptions": "Search options", + "noOptions": "No matching options." + }, + "ar-EG": { + "controls": "التحكم", + "configuration": "ملف الإعدادات", + "help": "المساعدة", + "settings": "الإعدادات", + "language": "اللغة", + "close": "قفل", + "eyebrow": "الملحقات / تخصيص التحكم", + "intro": "اختار زرار، وبعدين حدد له زرار من الكيبورد أو الماوس.", + "search": "دور على زرار", + "import": "استيراد INI", + "reset": "إعادة ضبط", + "group0": "أزرار التحكم", + "group1": "الأزرار العلوية", + "group2": "الاتجاهات", + "group3": "الانالوج الشمال", + "group4": "الانالوج اليمين", + "group5": "اللمس وأدوات تانية", + "select": "اختار…", + "add": "+ ضيف زرار", + "capture": "تسجيل زرار", + "remove": "إزالة", + "builtin": "الافتراضي: {inputs}", + "custom": "الزرار اللي اخترته بيستبدل التحكم الافتراضي.", + "noResults": "مفيش أزرار مطابقة للبحث.", + "download": "تنزيل INI", + "copy": "نسخ الإعدادات", + "previewEmpty": "ضيف زرار عشان تبدأ.", + "backControls": "ارجع للتحكم", + "switchGroup": "بدّل المجموعة", + "moveFocus": "انقل التركيز", + "helpIntro": "إجابات سريعة تساعدك تبدأ.", + "configIntro": "راجع اختياراتك، وبعدين انسخ الملف أو نزّله.", + "captureTitle": "اضغط زرار لـ {action}", + "cancelCapture": "إلغاء التسجيل", + "keepEditing": "كمّل التعديل", + "discard": "تجاهل التعديلات", + "faq0Question": "أربط زرار إزاي؟", + "faq0Answer": "اختار المجموعة واضغط ضيف زرار. بعدها اضغط تسجيل زرار أو اختاره من القائمة. تقدر تضيف أكتر من زرار لنفس الحركة.", + "faq1Question": "أحط ملف INI فين؟", + "faq1Answer": "نزّل anyps5-input.ini وحطه جنب ملف تشغيل اللعبة اللي اتولّد. لو عايزه في مكان تاني، حط مساره في ANYPS5_INPUT_CONFIG.", + "faq2Question": "ليه فيه زرار مش بيتسجل؟", + "faq2Answer": "ممكن المتصفح أو النظام يكون بيستخدمه كاختصار. اختاره من القائمة يدويًا. Escape بيلغي التسجيل؛ لو عايز تربطه اختاره من القائمة.", + "faq3Question": "تعديلاتي بتتحفظ لوحدها؟", + "faq3Answer": "التعديلات بتفضل في الذاكرة بس: انسخها أو نزّلها قبل تحديث الصفحة. اللغة بس هي اللي بتتحفظ في المتصفح. الأزرار اللي ماعدّلتهاش بتفضل على الافتراضي.", + "skip": "روح للمحتوى", + "mainNav": "التنقل الرئيسي", + "groups": "مجموعات التحكم", + "find": "دور على زرار", + "clock": "الوقت الحالي", + "output": "ملف الإعدادات", + "captureHelp": "التسجيل بيستخدم مكان الزرار على الكيبورد. Escape بيلغي التسجيل؛ عشان تربطه اختاره من القائمة. بعض اختصارات المتصفح والنظام لازم تختارها يدويًا.", + "replaceTitle": "تتجاهل التعديلات اللي لسه ماحفظتهاش؟", + "replaceHelp": "انسخ الإعدادات أو نزّلها الأول لو عايز تحتفظ بيها.", + "captureCancelled": "تم إلغاء تسجيل الزرار.", + "captureBlur": "التسجيل اتلغى عشان خرجت من النافذة.", + "captured": "تم تسجيل {value} لـ {action}.", + "resetDone": "الإعدادات رجعت للافتراضي.", + "importRejected": "الملف مااتقبلش", + "importRead": "الملف اتقري بنجاح", + "line": "سطر {line}: {message}", + "preserved": "إعداداتك الحالية لسه زي ما هي.", + "imported": "تم استيراد {name}.", + "importFailed": "الاستيراد ماكملش: {message}", + "fileTooLarge": "ملف INI لازم يكون أصغر من 1 MiB.", + "invalidUtf8": "الملف لازم يكون نص بترميز UTF-8 سليم.", + "downloaded": "تم طلب تنزيل الإعدادات.", + "copied": "الإعدادات اتنسخت.", + "clipboardFailed": "النسخ مش متاح. انسخ النص المحدد بـ Ctrl+C.", + "noScript": "فعّل JavaScript عشان تستخدم الصفحة.", + "noDefault": "مفيش زرار كيبورد أو ماوس افتراضي", + "bindingLabel": "{action}، الربط {number}", + "typeLabel": "نوع الإدخال لـ {binding}", + "valueLabel": "الزرار لـ {binding}", + "addLabel": "ضيف زرار لـ {action}", + "captureLabel": "سجّل زرار لـ {binding}", + "removeLabel": "شيل {binding}", + "searchOptions": "دور في القائمة", + "noOptions": "مفيش اختيار مطابق للبحث." + }, + "es": { + "controls": "Controles", + "configuration": "Configuración", + "help": "Ayuda", + "settings": "Ajustes", + "language": "Idioma", + "close": "Cerrar", + "eyebrow": "Accesorios / Controles personalizados", + "intro": "Elige un control y asigna entradas del teclado o ratón.", + "search": "Buscar controles", + "import": "Importar INI", + "reset": "Restablecer", + "group0": "Botones frontales", + "group1": "Botones superiores", + "group2": "Cruceta", + "group3": "Stick izquierdo", + "group4": "Stick derecho", + "group5": "Táctil y utilidades", + "select": "Seleccionar…", + "add": "+ Añadir entrada", + "capture": "Capturar", + "remove": "Eliminar", + "builtin": "Predeterminado: {inputs}", + "custom": "Las entradas personalizadas sustituyen las predeterminadas.", + "noResults": "No se encontraron controles.", + "download": "Descargar INI", + "copy": "Copiar configuración", + "previewEmpty": "Añade una entrada para empezar.", + "backControls": "Volver a controles", + "switchGroup": "Cambiar grupo", + "moveFocus": "Mover el foco", + "helpIntro": "Respuestas rápidas para empezar.", + "configIntro": "Revisa las entradas y copia o descarga el archivo.", + "captureTitle": "Pulsa una tecla para {action}", + "cancelCapture": "Cancelar captura", + "keepEditing": "Seguir editando", + "discard": "Descartar cambios", + "faq0Question": "¿Cómo asigno una tecla?", + "faq0Answer": "Elige un grupo y Añadir entrada. Usa Capturar o la lista. Puedes asignar varias entradas a una acción.", + "faq1Question": "¿Dónde guardo el archivo INI?", + "faq1Answer": "Pon anyps5-input.ini junto al ejecutable generado del juego. Para otra ruta, usa ANYPS5_INPUT_CONFIG.", + "faq2Question": "¿Por qué no se captura una tecla?", + "faq2Answer": "El navegador o sistema puede interceptarla. Elígela en la lista. Escape cancela la captura; asígnala desde la lista.", + "faq3Question": "¿Se guardan mis cambios solos?", + "faq3Answer": "Los cambios están en memoria: cópialos o descárgalos antes de recargar. Solo se guarda el idioma. Las acciones no configuradas conservan sus valores predeterminados.", + "searchOptions": "Buscar opciones", + "noOptions": "No hay opciones coincidentes." + }, + "fr": { + "controls": "Commandes", + "configuration": "Configuration", + "help": "Aide", + "settings": "Paramètres", + "language": "Langue", + "close": "Fermer", + "eyebrow": "Accessoires / Commandes personnalisées", + "intro": "Choisissez une commande, puis une touche ou un bouton de souris.", + "search": "Rechercher une commande", + "import": "Importer INI", + "reset": "Réinitialiser", + "group0": "Boutons principaux", + "group1": "Boutons supérieurs", + "group2": "Croix directionnelle", + "group3": "Stick gauche", + "group4": "Stick droit", + "group5": "Tactile et utilitaires", + "select": "Choisir…", + "add": "+ Ajouter une entrée", + "capture": "Capturer", + "remove": "Supprimer", + "builtin": "Par défaut : {inputs}", + "custom": "Les entrées personnalisées remplacent celles par défaut.", + "noResults": "Aucune commande trouvée.", + "download": "Télécharger INI", + "copy": "Copier la configuration", + "previewEmpty": "Ajoutez une entrée pour commencer.", + "backControls": "Retour aux commandes", + "switchGroup": "Changer de groupe", + "moveFocus": "Déplacer le focus", + "helpIntro": "Quelques réponses pour bien commencer.", + "configIntro": "Vérifiez les entrées, puis copiez ou téléchargez le fichier.", + "captureTitle": "Appuyez sur une touche pour {action}", + "cancelCapture": "Annuler la capture", + "keepEditing": "Continuer la modification", + "discard": "Ignorer les modifications", + "faq0Question": "Comment attribuer une touche ?", + "faq0Answer": "Choisissez un groupe et Ajouter une entrée. Utilisez Capturer ou la liste. Plusieurs entrées peuvent déclencher la même action.", + "faq1Question": "Où placer le fichier INI ?", + "faq1Answer": "Placez anyps5-input.ini près de l’exécutable généré du jeu. Pour un autre chemin, utilisez ANYPS5_INPUT_CONFIG.", + "faq2Question": "Pourquoi une touche n’est-elle pas capturée ?", + "faq2Answer": "Un raccourci du navigateur ou du système peut l’intercepter. Choisissez-la dans la liste. Escape annule la capture ; attribuez-la via la liste.", + "faq3Question": "Mes modifications sont-elles enregistrées ?", + "faq3Answer": "Les modifications restent en mémoire : copiez-les ou téléchargez-les avant de recharger. Seule la langue est mémorisée. Les actions non configurées gardent leurs valeurs par défaut.", + "searchOptions": "Rechercher une option", + "noOptions": "Aucune option trouvée." + }, + "de": { + "controls": "Steuerung", + "configuration": "Konfiguration", + "help": "Hilfe", + "settings": "Einstellungen", + "language": "Sprache", + "close": "Schließen", + "eyebrow": "Zubehör / Eigene Steuerung", + "intro": "Wähle eine Steuerung und weise Tastatur- oder Mauseingaben zu.", + "search": "Steuerung suchen", + "import": "INI importieren", + "reset": "Zurücksetzen", + "group0": "Aktionstasten", + "group1": "Schultertasten", + "group2": "Steuerkreuz", + "group3": "Linker Stick", + "group4": "Rechter Stick", + "group5": "Touch und Werkzeuge", + "select": "Auswählen…", + "add": "+ Eingabe hinzufügen", + "capture": "Erfassen", + "remove": "Entfernen", + "builtin": "Standard: {inputs}", + "custom": "Eigene Eingaben ersetzen die Standardeingaben.", + "noResults": "Keine passende Steuerung gefunden.", + "download": "INI herunterladen", + "copy": "Konfiguration kopieren", + "previewEmpty": "Füge eine Eingabe hinzu, um zu beginnen.", + "backControls": "Zurück zur Steuerung", + "switchGroup": "Gruppe wechseln", + "moveFocus": "Fokus verschieben", + "helpIntro": "Kurze Antworten für den Einstieg.", + "configIntro": "Prüfe die Eingaben und kopiere oder lade die Datei herunter.", + "captureTitle": "Drücke eine Taste für {action}", + "cancelCapture": "Erfassung abbrechen", + "keepEditing": "Weiter bearbeiten", + "discard": "Änderungen verwerfen", + "faq0Question": "Wie weise ich eine Taste zu?", + "faq0Answer": "Wähle eine Gruppe und Eingabe hinzufügen. Nutze Erfassen oder die Liste. Mehrere Eingaben können dieselbe Aktion auslösen.", + "faq1Question": "Wohin gehört die INI-Datei?", + "faq1Answer": "Lege anyps5-input.ini neben die erzeugte Spielprogrammdatei. Für einen anderen Pfad nutze ANYPS5_INPUT_CONFIG.", + "faq2Question": "Warum wird eine Taste nicht erfasst?", + "faq2Answer": "Browser- oder Systemkürzel können sie abfangen. Wähle sie in der Liste. Escape bricht die Erfassung ab; weise Escape über die Liste zu.", + "faq3Question": "Werden Änderungen automatisch gespeichert?", + "faq3Answer": "Änderungen bleiben im Speicher: vor dem Neuladen kopieren oder herunterladen. Nur die Sprache wird gespeichert. Nicht konfigurierte Aktionen behalten ihre Standardwerte.", + "searchOptions": "Optionen suchen", + "noOptions": "Keine passenden Optionen." + }, + "it": { + "controls": "Controlli", + "configuration": "Configurazione", + "help": "Aiuto", + "settings": "Impostazioni", + "language": "Lingua", + "close": "Chiudi", + "eyebrow": "Accessori / Controlli personalizzati", + "intro": "Scegli un controllo e assegna un tasto o un pulsante del mouse.", + "search": "Cerca controlli", + "import": "Importa INI", + "reset": "Reimposta", + "group0": "Pulsanti principali", + "group1": "Pulsanti superiori", + "group2": "Croce direzionale", + "group3": "Stick sinistro", + "group4": "Stick destro", + "group5": "Touch e utilità", + "select": "Seleziona…", + "add": "+ Aggiungi input", + "capture": "Acquisisci", + "remove": "Rimuovi", + "builtin": "Predefinito: {inputs}", + "custom": "Gli input personalizzati sostituiscono quelli predefiniti.", + "noResults": "Nessun controllo trovato.", + "download": "Scarica INI", + "copy": "Copia configurazione", + "previewEmpty": "Aggiungi un input per iniziare.", + "backControls": "Torna ai controlli", + "switchGroup": "Cambia gruppo", + "moveFocus": "Sposta il focus", + "helpIntro": "Risposte rapide per iniziare.", + "configIntro": "Controlla gli input e copia o scarica il file.", + "captureTitle": "Premi un tasto per {action}", + "cancelCapture": "Annulla acquisizione", + "keepEditing": "Continua a modificare", + "discard": "Scarta modifiche", + "faq0Question": "Come assegno un tasto?", + "faq0Answer": "Scegli un gruppo e Aggiungi input. Usa Acquisisci o l’elenco. Puoi assegnare più input alla stessa azione.", + "faq1Question": "Dove metto il file INI?", + "faq1Answer": "Metti anyps5-input.ini accanto all’eseguibile generato del gioco. Per un altro percorso, usa ANYPS5_INPUT_CONFIG.", + "faq2Question": "Perché un tasto non viene acquisito?", + "faq2Answer": "Il browser o il sistema potrebbe intercettarlo. Sceglilo dall’elenco. Escape annulla l’acquisizione; assegnalo dall’elenco.", + "faq3Question": "Le modifiche si salvano automaticamente?", + "faq3Answer": "Le modifiche restano in memoria: copiale o scaricale prima di ricaricare. Si salva solo la lingua. Le azioni non configurate mantengono i valori predefiniti.", + "searchOptions": "Cerca opzioni", + "noOptions": "Nessuna opzione trovata." + }, + "pt": { + "controls": "Controles", + "configuration": "Configuração", + "help": "Ajuda", + "settings": "Configurações", + "language": "Idioma", + "close": "Fechar", + "eyebrow": "Acessórios / Controles personalizados", + "intro": "Escolha um controle e atribua entradas do teclado ou mouse.", + "search": "Buscar controles", + "import": "Importar INI", + "reset": "Redefinir", + "group0": "Botões principais", + "group1": "Botões superiores", + "group2": "Direcional", + "group3": "Analógico esquerdo", + "group4": "Analógico direito", + "group5": "Toque e utilidades", + "select": "Selecionar…", + "add": "+ Adicionar entrada", + "capture": "Capturar", + "remove": "Remover", + "builtin": "Padrão: {inputs}", + "custom": "As entradas personalizadas substituem as entradas padrão.", + "noResults": "Nenhum controle encontrado.", + "download": "Baixar INI", + "copy": "Copiar configuração", + "previewEmpty": "Adicione uma entrada para começar.", + "backControls": "Voltar aos controles", + "switchGroup": "Trocar grupo", + "moveFocus": "Mover o foco", + "helpIntro": "Respostas rápidas para começar.", + "configIntro": "Confira as entradas e copie ou baixe o arquivo.", + "captureTitle": "Pressione uma tecla para {action}", + "cancelCapture": "Cancelar captura", + "keepEditing": "Continuar editando", + "discard": "Descartar alterações", + "faq0Question": "Como atribuo uma tecla?", + "faq0Answer": "Escolha um grupo e Adicionar entrada. Use Capturar ou a lista. Uma ação pode ter várias entradas alternativas.", + "faq1Question": "Onde coloco o arquivo INI?", + "faq1Answer": "Coloque anyps5-input.ini junto do executável gerado do jogo. Para outro caminho, use ANYPS5_INPUT_CONFIG.", + "faq2Question": "Por que uma tecla não é capturada?", + "faq2Answer": "O navegador ou sistema pode interceptá-la. Escolha na lista. Escape cancela a captura; atribua Escape pela lista.", + "faq3Question": "As alterações são salvas sozinhas?", + "faq3Answer": "As alterações ficam na memória: copie ou baixe antes de recarregar. Só o idioma é salvo. Ações não configuradas mantêm os padrões.", + "searchOptions": "Buscar opções", + "noOptions": "Nenhuma opção encontrada." + }, + "ru": { + "controls": "Управление", + "configuration": "Конфигурация", + "help": "Помощь", + "settings": "Настройки", + "language": "Язык", + "close": "Закрыть", + "eyebrow": "Аксессуары / Настройка управления", + "intro": "Выберите действие и назначьте клавишу или кнопку мыши.", + "search": "Поиск действий", + "import": "Импорт INI", + "reset": "Сбросить", + "group0": "Основные кнопки", + "group1": "Верхние кнопки", + "group2": "Крестовина", + "group3": "Левый стик", + "group4": "Правый стик", + "group5": "Сенсор и инструменты", + "select": "Выбрать…", + "add": "+ Добавить ввод", + "capture": "Записать", + "remove": "Удалить", + "builtin": "По умолчанию: {inputs}", + "custom": "Новые назначения заменяют стандартные.", + "noResults": "Действия не найдены.", + "download": "Скачать INI", + "copy": "Копировать конфигурацию", + "previewEmpty": "Добавьте назначение, чтобы начать.", + "backControls": "Назад к управлению", + "switchGroup": "Сменить группу", + "moveFocus": "Переместить фокус", + "helpIntro": "Краткие ответы для начала работы.", + "configIntro": "Проверьте назначения и скопируйте или скачайте файл.", + "captureTitle": "Нажмите клавишу для {action}", + "cancelCapture": "Отменить запись", + "keepEditing": "Продолжить изменение", + "discard": "Отменить изменения", + "faq0Question": "Как назначить клавишу?", + "faq0Answer": "Выберите группу и Добавить ввод. Используйте Записать или список. Одно действие может иметь несколько назначений.", + "faq1Question": "Куда поместить INI-файл?", + "faq1Answer": "Поместите anyps5-input.ini рядом с созданным исполняемым файлом игры. Для другого пути используйте ANYPS5_INPUT_CONFIG.", + "faq2Question": "Почему клавиша не записывается?", + "faq2Answer": "Браузер или система может перехватывать её. Выберите клавишу в списке. Escape отменяет запись; назначайте Escape через список.", + "faq3Question": "Изменения сохраняются автоматически?", + "faq3Answer": "Изменения хранятся в памяти: копируйте или скачивайте их перед перезагрузкой. Сохраняется только язык. Ненастроенные действия используют значения по умолчанию.", + "searchOptions": "Поиск вариантов", + "noOptions": "Совпадений нет." + }, + "tr": { + "controls": "Kontroller", + "configuration": "Yapılandırma", + "help": "Yardım", + "settings": "Ayarlar", + "language": "Dil", + "close": "Kapat", + "eyebrow": "Aksesuarlar / Özel kontroller", + "intro": "Bir kontrol seçip klavye veya fare girdisi atayın.", + "search": "Kontrol ara", + "import": "INI içe aktar", + "reset": "Sıfırla", + "group0": "Ana düğmeler", + "group1": "Üst düğmeler", + "group2": "Yön tuşları", + "group3": "Sol çubuk", + "group4": "Sağ çubuk", + "group5": "Dokunma ve araçlar", + "select": "Seç…", + "add": "+ Girdi ekle", + "capture": "Yakala", + "remove": "Kaldır", + "builtin": "Varsayılan: {inputs}", + "custom": "Özel girdiler varsayılanların yerini alır.", + "noResults": "Kontrol bulunamadı.", + "download": "INI indir", + "copy": "Yapılandırmayı kopyala", + "previewEmpty": "Başlamak için bir girdi ekleyin.", + "backControls": "Kontrollere dön", + "switchGroup": "Grubu değiştir", + "moveFocus": "Odağı taşı", + "helpIntro": "Başlamanıza yardımcı olacak kısa yanıtlar.", + "configIntro": "Girdileri kontrol edip dosyayı kopyalayın veya indirin.", + "captureTitle": "{action} için bir tuşa basın", + "cancelCapture": "Yakalamayı iptal et", + "keepEditing": "Düzenlemeye devam et", + "discard": "Değişiklikleri sil", + "faq0Question": "Nasıl tuş atarım?", + "faq0Answer": "Bir grup seçip Girdi ekle düğmesine basın. Yakala veya listeyi kullanın. Bir eyleme birden fazla girdi atanabilir.", + "faq1Question": "INI dosyasını nereye koyarım?", + "faq1Answer": "anyps5-input.ini dosyasını oluşturulan oyun yürütülebilir dosyasının yanına koyun. Başka yol için ANYPS5_INPUT_CONFIG kullanın.", + "faq2Question": "Neden bir tuş yakalanmıyor?", + "faq2Answer": "Tarayıcı veya sistem kısayolu engelleyebilir. Listeden seçin. Escape yakalamayı iptal eder; Escape tuşunu listeden atayın.", + "faq3Question": "Değişiklikler otomatik kaydedilir mi?", + "faq3Answer": "Değişiklikler bellekte kalır: yenilemeden önce kopyalayın veya indirin. Yalnızca dil kaydedilir. Ayarlanmayan eylemler varsayılanlarını korur.", + "searchOptions": "Seçenek ara", + "noOptions": "Eşleşen seçenek yok." + }, + "zh-Hans": { + "controls": "控制", + "configuration": "配置", + "help": "帮助", + "settings": "设置", + "language": "语言", + "close": "关闭", + "eyebrow": "配件 / 自定义控制", + "intro": "选择一个控制,然后分配键盘或鼠标输入。", + "search": "搜索控制", + "import": "导入 INI", + "reset": "重置", + "group0": "主要按钮", + "group1": "肩部按钮", + "group2": "方向键", + "group3": "左摇杆", + "group4": "右摇杆", + "group5": "触摸和工具", + "select": "选择…", + "add": "+ 添加输入", + "capture": "录入", + "remove": "移除", + "builtin": "默认:{inputs}", + "custom": "自定义输入会替换默认输入。", + "noResults": "没有找到匹配的控制。", + "download": "下载 INI", + "copy": "复制配置", + "previewEmpty": "添加输入以开始。", + "backControls": "返回控制", + "switchGroup": "切换分组", + "moveFocus": "移动焦点", + "helpIntro": "帮助您入门的简短解答。", + "configIntro": "检查输入,然后复制或下载文件。", + "captureTitle": "为 {action} 按下一个键", + "cancelCapture": "取消录入", + "keepEditing": "继续编辑", + "discard": "放弃更改", + "faq0Question": "如何分配按键?", + "faq0Answer": "选择分组和添加输入,然后使用录入或列表。同一操作可以使用多个输入。", + "faq1Question": "INI 文件放在哪里?", + "faq1Answer": "将 anyps5-input.ini 放在生成的游戏可执行文件旁。其他位置可使用 ANYPS5_INPUT_CONFIG 指定路径。", + "faq2Question": "为什么无法录入某个键?", + "faq2Answer": "浏览器或系统快捷键可能会拦截它。请从列表选择。Escape 会取消录入;要分配 Escape,请使用列表。", + "faq3Question": "修改会自动保存吗?", + "faq3Answer": "输入修改只保留在内存中,刷新前请复制或下载。浏览器只保存语言选择。未配置的操作保留默认输入。", + "searchOptions": "搜索选项", + "noOptions": "没有匹配选项。" + }, + "ja": { + "controls": "操作", + "configuration": "構成", + "help": "ヘルプ", + "settings": "設定", + "language": "言語", + "close": "閉じる", + "eyebrow": "アクセサリー / 操作のカスタマイズ", + "intro": "操作を選び、キーボードやマウスの入力を割り当てます。", + "search": "操作を検索", + "import": "INI を読み込む", + "reset": "リセット", + "group0": "メインボタン", + "group1": "ショルダーボタン", + "group2": "方向キー", + "group3": "左スティック", + "group4": "右スティック", + "group5": "タッチとツール", + "select": "選択…", + "add": "+ 入力を追加", + "capture": "記録", + "remove": "削除", + "builtin": "初期設定:{inputs}", + "custom": "カスタム入力は初期設定を置き換えます。", + "noResults": "一致する操作がありません。", + "download": "INI をダウンロード", + "copy": "構成をコピー", + "previewEmpty": "入力を追加して開始します。", + "backControls": "操作に戻る", + "switchGroup": "グループを切り替え", + "moveFocus": "フォーカスを移動", + "helpIntro": "使い始めるための簡単な説明です。", + "configIntro": "入力を確認してファイルをコピーまたはダウンロードします。", + "captureTitle": "{action} のキーを押してください", + "cancelCapture": "記録をキャンセル", + "keepEditing": "編集を続ける", + "discard": "変更を破棄", + "faq0Question": "キーを割り当てるには?", + "faq0Answer": "グループと入力を追加を選び、記録または一覧を使います。同じ操作に複数の入力を割り当てられます。", + "faq1Question": "INI ファイルはどこに置きますか?", + "faq1Answer": "anyps5-input.ini を生成されたゲーム実行ファイルの隣に置きます。別の場所には ANYPS5_INPUT_CONFIG でパスを指定します。", + "faq2Question": "キーが記録されないのはなぜですか?", + "faq2Answer": "ブラウザーやシステムのショートカットが優先される場合があります。一覧から選んでください。Escape は記録をキャンセルするため、一覧から割り当てます。", + "faq3Question": "変更は自動保存されますか?", + "faq3Answer": "入力の変更はメモリ内だけです。再読み込み前にコピーかダウンロードをしてください。言語のみ保存されます。未設定の操作は初期設定を使います。", + "searchOptions": "選択肢を検索", + "noOptions": "一致する選択肢がありません。" + }, + "ko": { + "controls": "컨트롤", + "configuration": "구성", + "help": "도움말", + "settings": "설정", + "language": "언어", + "close": "닫기", + "eyebrow": "액세서리 / 컨트롤 맞춤 설정", + "intro": "컨트롤을 선택하고 키보드 또는 마우스 입력을 지정하세요.", + "search": "컨트롤 검색", + "import": "INI 가져오기", + "reset": "초기화", + "group0": "주요 버튼", + "group1": "상단 버튼", + "group2": "방향키", + "group3": "왼쪽 스틱", + "group4": "오른쪽 스틱", + "group5": "터치 및 도구", + "select": "선택…", + "add": "+ 입력 추가", + "capture": "기록", + "remove": "제거", + "builtin": "기본값: {inputs}", + "custom": "사용자 입력이 기본 입력을 대체합니다.", + "noResults": "일치하는 컨트롤이 없습니다.", + "download": "INI 다운로드", + "copy": "구성 복사", + "previewEmpty": "입력을 추가하여 시작하세요.", + "backControls": "컨트롤로 돌아가기", + "switchGroup": "그룹 전환", + "moveFocus": "포커스 이동", + "helpIntro": "시작을 돕는 간단한 답변입니다.", + "configIntro": "입력을 확인하고 파일을 복사하거나 다운로드하세요.", + "captureTitle": "{action}에 사용할 키를 누르세요", + "cancelCapture": "기록 취소", + "keepEditing": "계속 편집", + "discard": "변경 사항 삭제", + "faq0Question": "키는 어떻게 지정하나요?", + "faq0Answer": "그룹과 입력 추가를 선택한 뒤 기록 또는 목록을 사용하세요. 같은 동작에 여러 입력을 지정할 수 있습니다.", + "faq1Question": "INI 파일은 어디에 두나요?", + "faq1Answer": "생성된 게임 실행 파일 옆에 anyps5-input.ini를 두세요. 다른 경로는 ANYPS5_INPUT_CONFIG로 지정하세요.", + "faq2Question": "키가 기록되지 않는 이유는 무엇인가요?", + "faq2Answer": "브라우저나 시스템 단축키가 가로챌 수 있습니다. 목록에서 선택하세요. Escape는 기록을 취소하므로 목록에서 지정하세요.", + "faq3Question": "변경 사항이 자동 저장되나요?", + "faq3Answer": "입력 변경은 메모리에만 있습니다. 새로고침 전에 복사하거나 다운로드하세요. 언어 선택만 저장됩니다. 설정하지 않은 동작은 기본값을 유지합니다.", + "searchOptions": "옵션 검색", + "noOptions": "일치하는 옵션이 없습니다." + }, + "hi": { + "controls": "नियंत्रण", + "configuration": "कॉन्फ़िगरेशन", + "help": "मदद", + "settings": "सेटिंग्स", + "language": "भाषा", + "close": "बंद करें", + "eyebrow": "सहायक उपकरण / कस्टम नियंत्रण", + "intro": "नियंत्रण चुनें और कीबोर्ड या माउस इनपुट जोड़ें।", + "search": "नियंत्रण खोजें", + "import": "INI आयात करें", + "reset": "रीसेट", + "group0": "मुख्य बटन", + "group1": "ऊपरी बटन", + "group2": "दिशा बटन", + "group3": "बायाँ स्टिक", + "group4": "दायाँ स्टिक", + "group5": "टच और उपकरण", + "select": "चुनें…", + "add": "+ इनपुट जोड़ें", + "capture": "रिकॉर्ड करें", + "remove": "हटाएँ", + "builtin": "डिफ़ॉल्ट: {inputs}", + "custom": "कस्टम इनपुट डिफ़ॉल्ट इनपुट की जगह लेते हैं।", + "noResults": "कोई नियंत्रण नहीं मिला।", + "download": "INI डाउनलोड करें", + "copy": "कॉन्फ़िगरेशन कॉपी करें", + "previewEmpty": "शुरू करने के लिए इनपुट जोड़ें।", + "backControls": "नियंत्रण पर वापस जाएँ", + "switchGroup": "समूह बदलें", + "moveFocus": "फ़ोकस बदलें", + "helpIntro": "शुरू करने के लिए कुछ आसान जवाब।", + "configIntro": "इनपुट जाँचें और फ़ाइल कॉपी या डाउनलोड करें।", + "captureTitle": "{action} के लिए कोई कुंजी दबाएँ", + "cancelCapture": "रिकॉर्डिंग रद्द करें", + "keepEditing": "संपादन जारी रखें", + "discard": "बदलाव हटाएँ", + "faq0Question": "कुंजी कैसे जोड़ूँ?", + "faq0Answer": "समूह और इनपुट जोड़ें चुनें, फिर रिकॉर्ड करें या सूची का उपयोग करें। एक काम के लिए कई इनपुट जोड़ सकते हैं।", + "faq1Question": "INI फ़ाइल कहाँ रखें?", + "faq1Answer": "anyps5-input.ini को बनाए गए गेम प्रोग्राम के पास रखें। दूसरे स्थान के लिए ANYPS5_INPUT_CONFIG में पथ दें।", + "faq2Question": "कुंजी रिकॉर्ड क्यों नहीं होती?", + "faq2Answer": "ब्राउज़र या सिस्टम का शॉर्टकट उसे रोक सकता है। सूची से चुनें। Escape रिकॉर्डिंग रद्द करता है; इसे सूची से जोड़ें।", + "faq3Question": "क्या बदलाव अपने आप सेव होते हैं?", + "faq3Answer": "बदलाव केवल मेमोरी में रहते हैं: रीलोड से पहले कॉपी या डाउनलोड करें। केवल भाषा सेव होती है। बिना बदले काम अपने डिफ़ॉल्ट रखते हैं।", + "searchOptions": "विकल्प खोजें", + "noOptions": "कोई विकल्प नहीं मिला।" + }, + "bn": { + "controls": "নিয়ন্ত্রণ", + "configuration": "কনফিগারেশন", + "help": "সাহায্য", + "settings": "সেটিংস", + "language": "ভাষা", + "close": "বন্ধ করুন", + "eyebrow": "আনুষঙ্গিক / নিজস্ব নিয়ন্ত্রণ", + "intro": "একটি নিয়ন্ত্রণ বেছে কিবোর্ড বা মাউস ইনপুট দিন।", + "search": "নিয়ন্ত্রণ খুঁজুন", + "import": "INI আমদানি", + "reset": "রিসেট", + "group0": "প্রধান বোতাম", + "group1": "ওপরের বোতাম", + "group2": "দিক বোতাম", + "group3": "বাম স্টিক", + "group4": "ডান স্টিক", + "group5": "টাচ ও সরঞ্জাম", + "select": "বাছুন…", + "add": "+ ইনপুট যোগ", + "capture": "রেকর্ড", + "remove": "সরান", + "builtin": "ডিফল্ট: {inputs}", + "custom": "নিজস্ব ইনপুট ডিফল্ট ইনপুটের বদলে কাজ করে।", + "noResults": "কোনো নিয়ন্ত্রণ পাওয়া যায়নি।", + "download": "INI ডাউনলোড", + "copy": "কনফিগারেশন কপি", + "previewEmpty": "শুরু করতে একটি ইনপুট যোগ করুন।", + "backControls": "নিয়ন্ত্রণে ফিরে যান", + "switchGroup": "গ্রুপ বদলান", + "moveFocus": "ফোকাস সরান", + "helpIntro": "শুরু করার জন্য কিছু সহজ উত্তর।", + "configIntro": "ইনপুট দেখে ফাইল কপি বা ডাউনলোড করুন।", + "captureTitle": "{action}-এর জন্য একটি কী চাপুন", + "cancelCapture": "রেকর্ড বাতিল", + "keepEditing": "সম্পাদনা চালান", + "discard": "পরিবর্তন বাদ দিন", + "faq0Question": "কী কীভাবে যোগ করব?", + "faq0Answer": "গ্রুপ ও ইনপুট যোগ বেছে রেকর্ড বা তালিকা ব্যবহার করুন। এক কাজের জন্য একাধিক ইনপুট দিতে পারেন।", + "faq1Question": "INI ফাইল কোথায় রাখব?", + "faq1Answer": "তৈরি হওয়া গেম চালানোর ফাইলের পাশে anyps5-input.ini রাখুন। অন্য পথের জন্য ANYPS5_INPUT_CONFIG ব্যবহার করুন।", + "faq2Question": "কোনো কী রেকর্ড হচ্ছে না কেন?", + "faq2Answer": "ব্রাউজার বা সিস্টেম শর্টকাট সেটি আটকাতে পারে। তালিকা থেকে বাছুন। Escape রেকর্ড বাতিল করে; তালিকা থেকে যোগ করুন।", + "faq3Question": "পরিবর্তন কি নিজে থেকে সেভ হয়?", + "faq3Answer": "পরিবর্তন শুধু মেমোরিতে থাকে: রিলোডের আগে কপি বা ডাউনলোড করুন। শুধু ভাষা সেভ হয়। না বদলানো কাজগুলো ডিফল্ট রাখে।", + "searchOptions": "বিকল্প খুঁজুন", + "noOptions": "কোনো বিকল্প মেলেনি।" + }, + "ur": { + "controls": "کنٹرول", + "configuration": "ترتیب", + "help": "مدد", + "settings": "سیٹنگز", + "language": "زبان", + "close": "بند کریں", + "eyebrow": "لوازمات / حسب ضرورت کنٹرول", + "intro": "کنٹرول منتخب کرکے کی بورڈ یا ماؤس ان پٹ مقرر کریں۔", + "search": "کنٹرول تلاش کریں", + "import": "INI درآمد کریں", + "reset": "ری سیٹ", + "group0": "بنیادی بٹن", + "group1": "اوپری بٹن", + "group2": "سمت کے بٹن", + "group3": "بائیں اسٹک", + "group4": "دائیں اسٹک", + "group5": "ٹچ اور اوزار", + "select": "منتخب کریں…", + "add": "+ ان پٹ شامل کریں", + "capture": "ریکارڈ کریں", + "remove": "ہٹائیں", + "builtin": "پہلے سے طے شدہ: {inputs}", + "custom": "حسب ضرورت ان پٹ پہلے سے طے شدہ ان پٹ کی جگہ لیتے ہیں۔", + "noResults": "کوئی کنٹرول نہیں ملا۔", + "download": "INI ڈاؤن لوڈ کریں", + "copy": "ترتیب کاپی کریں", + "previewEmpty": "شروع کرنے کے لیے ان پٹ شامل کریں۔", + "backControls": "کنٹرول پر واپس جائیں", + "switchGroup": "گروپ بدلیں", + "moveFocus": "فوکس بدلیں", + "helpIntro": "شروع کرنے کے لیے مختصر جوابات۔", + "configIntro": "ان پٹ دیکھ کر فائل کاپی یا ڈاؤن لوڈ کریں۔", + "captureTitle": "{action} کے لیے کوئی کلید دبائیں", + "cancelCapture": "ریکارڈنگ منسوخ کریں", + "keepEditing": "ترمیم جاری رکھیں", + "discard": "تبدیلیاں چھوڑ دیں", + "faq0Question": "کلید کیسے مقرر کروں؟", + "faq0Answer": "گروپ اور ان پٹ شامل کریں منتخب کریں، پھر ریکارڈ یا فہرست استعمال کریں۔ ایک عمل کے لیے کئی ان پٹ رکھ سکتے ہیں۔", + "faq1Question": "INI فائل کہاں رکھوں؟", + "faq1Answer": "anyps5-input.ini کو بنائی گئی گیم کی قابل اجرا فائل کے ساتھ رکھیں۔ دوسرے راستے کے لیے ANYPS5_INPUT_CONFIG استعمال کریں۔", + "faq2Question": "کلید ریکارڈ کیوں نہیں ہوتی؟", + "faq2Answer": "براؤزر یا نظام کا شارٹ کٹ روک سکتا ہے۔ فہرست سے منتخب کریں۔ Escape ریکارڈنگ منسوخ کرتا ہے؛ اسے فہرست سے مقرر کریں۔", + "faq3Question": "کیا تبدیلیاں خود محفوظ ہوتی ہیں؟", + "faq3Answer": "تبدیلیاں صرف میموری میں رہتی ہیں: ری لوڈ سے پہلے کاپی یا ڈاؤن لوڈ کریں۔ صرف زبان محفوظ ہوتی ہے۔ غیر تبدیل شدہ اعمال اپنی طے شدہ ترتیب رکھتے ہیں۔", + "searchOptions": "اختیارات تلاش کریں", + "noOptions": "کوئی اختیار نہیں ملا۔" + }, + "fa": { + "controls": "کنترل‌ها", + "configuration": "پیکربندی", + "help": "راهنما", + "settings": "تنظیمات", + "language": "زبان", + "close": "بستن", + "eyebrow": "لوازم جانبی / کنترل سفارشی", + "intro": "یک کنترل را انتخاب کنید و ورودی صفحه‌کلید یا ماوس بدهید.", + "search": "جستجوی کنترل", + "import": "وارد کردن INI", + "reset": "بازنشانی", + "group0": "دکمه‌های اصلی", + "group1": "دکمه‌های بالایی", + "group2": "دکمه‌های جهت", + "group3": "دسته چپ", + "group4": "دسته راست", + "group5": "لمس و ابزارها", + "select": "انتخاب…", + "add": "+ افزودن ورودی", + "capture": "ثبت", + "remove": "حذف", + "builtin": "پیش‌فرض: {inputs}", + "custom": "ورودی سفارشی جای ورودی پیش‌فرض را می‌گیرد.", + "noResults": "کنترلی پیدا نشد.", + "download": "دانلود INI", + "copy": "کپی پیکربندی", + "previewEmpty": "برای شروع یک ورودی اضافه کنید.", + "backControls": "بازگشت به کنترل‌ها", + "switchGroup": "تغییر گروه", + "moveFocus": "جابجایی تمرکز", + "helpIntro": "پاسخ‌های کوتاه برای شروع.", + "configIntro": "ورودی‌ها را بررسی و فایل را کپی یا دانلود کنید.", + "captureTitle": "برای {action} کلیدی فشار دهید", + "cancelCapture": "لغو ثبت", + "keepEditing": "ادامه ویرایش", + "discard": "کنار گذاشتن تغییرات", + "faq0Question": "چطور کلید تعیین کنم؟", + "faq0Answer": "گروه و افزودن ورودی را انتخاب کنید، سپس ثبت یا فهرست را به کار ببرید. یک عمل می‌تواند چند ورودی داشته باشد.", + "faq1Question": "فایل INI را کجا بگذارم؟", + "faq1Answer": "anyps5-input.ini را کنار فایل اجرایی ساخته‌شده بازی قرار دهید. برای مسیر دیگر از ANYPS5_INPUT_CONFIG استفاده کنید.", + "faq2Question": "چرا کلید ثبت نمی‌شود؟", + "faq2Answer": "میانبر مرورگر یا سیستم ممکن است آن را بگیرد. از فهرست انتخاب کنید. Escape ثبت را لغو می‌کند؛ آن را از فهرست تعیین کنید.", + "faq3Question": "آیا تغییرات خودکار ذخیره می‌شوند؟", + "faq3Answer": "تغییرات فقط در حافظه‌اند: قبل از بارگذاری دوباره کپی یا دانلود کنید. تنها زبان ذخیره می‌شود. اعمال تنظیم‌نشده پیش‌فرض خود را حفظ می‌کنند.", + "searchOptions": "جستجوی گزینه‌ها", + "noOptions": "گزینه‌ای پیدا نشد." + }, + "id": { + "controls": "Kontrol", + "configuration": "Konfigurasi", + "help": "Bantuan", + "settings": "Pengaturan", + "language": "Bahasa", + "close": "Tutup", + "eyebrow": "Aksesori / Kontrol khusus", + "intro": "Pilih kontrol, lalu tetapkan input keyboard atau mouse.", + "search": "Cari kontrol", + "import": "Impor INI", + "reset": "Atur ulang", + "group0": "Tombol utama", + "group1": "Tombol atas", + "group2": "Tombol arah", + "group3": "Stik kiri", + "group4": "Stik kanan", + "group5": "Sentuh dan alat", + "select": "Pilih…", + "add": "+ Tambah input", + "capture": "Rekam", + "remove": "Hapus", + "builtin": "Bawaan: {inputs}", + "custom": "Input khusus menggantikan input bawaan.", + "noResults": "Kontrol tidak ditemukan.", + "download": "Unduh INI", + "copy": "Salin konfigurasi", + "previewEmpty": "Tambahkan input untuk memulai.", + "backControls": "Kembali ke kontrol", + "switchGroup": "Ganti grup", + "moveFocus": "Pindahkan fokus", + "helpIntro": "Jawaban singkat untuk memulai.", + "configIntro": "Periksa input, lalu salin atau unduh berkas.", + "captureTitle": "Tekan tombol untuk {action}", + "cancelCapture": "Batalkan rekaman", + "keepEditing": "Lanjutkan mengedit", + "discard": "Buang perubahan", + "faq0Question": "Bagaimana menetapkan tombol?", + "faq0Answer": "Pilih grup dan Tambah input, lalu Rekam atau gunakan daftar. Satu aksi dapat memiliki beberapa input.", + "faq1Question": "Di mana berkas INI diletakkan?", + "faq1Answer": "Letakkan anyps5-input.ini di samping berkas program game yang dihasilkan. Untuk lokasi lain, gunakan ANYPS5_INPUT_CONFIG.", + "faq2Question": "Mengapa tombol tidak direkam?", + "faq2Answer": "Pintasan browser atau sistem mungkin menangkapnya. Pilih dari daftar. Escape membatalkan rekaman; tetapkan Escape melalui daftar.", + "faq3Question": "Apakah perubahan disimpan otomatis?", + "faq3Answer": "Perubahan hanya di memori: salin atau unduh sebelum memuat ulang. Hanya bahasa yang disimpan. Aksi yang tidak diatur tetap memakai bawaan.", + "searchOptions": "Cari opsi", + "noOptions": "Tidak ada opsi yang cocok." + }, + "vi": { + "controls": "Điều khiển", + "configuration": "Cấu hình", + "help": "Trợ giúp", + "settings": "Cài đặt", + "language": "Ngôn ngữ", + "close": "Đóng", + "eyebrow": "Phụ kiện / Điều khiển tùy chỉnh", + "intro": "Chọn điều khiển rồi gán phím hoặc nút chuột.", + "search": "Tìm điều khiển", + "import": "Nhập INI", + "reset": "Đặt lại", + "group0": "Nút chính", + "group1": "Nút phía trên", + "group2": "Phím hướng", + "group3": "Cần trái", + "group4": "Cần phải", + "group5": "Cảm ứng và công cụ", + "select": "Chọn…", + "add": "+ Thêm đầu vào", + "capture": "Ghi phím", + "remove": "Xóa", + "builtin": "Mặc định: {inputs}", + "custom": "Đầu vào tùy chỉnh thay thế đầu vào mặc định.", + "noResults": "Không tìm thấy điều khiển.", + "download": "Tải INI", + "copy": "Sao chép cấu hình", + "previewEmpty": "Thêm đầu vào để bắt đầu.", + "backControls": "Trở về điều khiển", + "switchGroup": "Đổi nhóm", + "moveFocus": "Di chuyển tiêu điểm", + "helpIntro": "Câu trả lời nhanh để bắt đầu.", + "configIntro": "Kiểm tra đầu vào rồi sao chép hoặc tải tệp.", + "captureTitle": "Nhấn phím cho {action}", + "cancelCapture": "Hủy ghi phím", + "keepEditing": "Tiếp tục chỉnh sửa", + "discard": "Bỏ thay đổi", + "faq0Question": "Làm sao gán phím?", + "faq0Answer": "Chọn nhóm và Thêm đầu vào, rồi dùng Ghi phím hoặc danh sách. Một hành động có thể có nhiều đầu vào.", + "faq1Question": "Đặt tệp INI ở đâu?", + "faq1Answer": "Đặt anyps5-input.ini cạnh tệp chạy trò chơi được tạo. Dùng ANYPS5_INPUT_CONFIG cho đường dẫn khác.", + "faq2Question": "Tại sao không ghi được phím?", + "faq2Answer": "Trình duyệt hoặc hệ thống có thể chặn phím tắt. Chọn từ danh sách. Escape hủy ghi phím; hãy gán Escape từ danh sách.", + "faq3Question": "Thay đổi có tự lưu không?", + "faq3Answer": "Thay đổi chỉ ở trong bộ nhớ: sao chép hoặc tải xuống trước khi tải lại. Chỉ ngôn ngữ được lưu. Hành động chưa cấu hình giữ mặc định.", + "searchOptions": "Tìm tùy chọn", + "noOptions": "Không có tùy chọn phù hợp." + }, + "th": { + "controls": "การควบคุม", + "configuration": "การกำหนดค่า", + "help": "ช่วยเหลือ", + "settings": "การตั้งค่า", + "language": "ภาษา", + "close": "ปิด", + "eyebrow": "อุปกรณ์เสริม / ปรับแต่งการควบคุม", + "intro": "เลือกการควบคุม แล้วกำหนดปุ่มแป้นพิมพ์หรือเมาส์", + "search": "ค้นหาการควบคุม", + "import": "นำเข้า INI", + "reset": "รีเซ็ต", + "group0": "ปุ่มหลัก", + "group1": "ปุ่มด้านบน", + "group2": "ปุ่มทิศทาง", + "group3": "สติ๊กซ้าย", + "group4": "สติ๊กขวา", + "group5": "สัมผัสและเครื่องมือ", + "select": "เลือก…", + "add": "+ เพิ่มอินพุต", + "capture": "บันทึกปุ่ม", + "remove": "ลบ", + "builtin": "ค่าเริ่มต้น: {inputs}", + "custom": "อินพุตที่กำหนดเองจะแทนที่ค่าเริ่มต้น", + "noResults": "ไม่พบการควบคุม", + "download": "ดาวน์โหลด INI", + "copy": "คัดลอกการกำหนดค่า", + "previewEmpty": "เพิ่มอินพุตเพื่อเริ่มต้น", + "backControls": "กลับไปการควบคุม", + "switchGroup": "เปลี่ยนกลุ่ม", + "moveFocus": "ย้ายโฟกัส", + "helpIntro": "คำตอบสั้นๆ เพื่อช่วยเริ่มต้น", + "configIntro": "ตรวจสอบอินพุต แล้วคัดลอกหรือดาวน์โหลดไฟล์", + "captureTitle": "กดปุ่มสำหรับ {action}", + "cancelCapture": "ยกเลิกการบันทึก", + "keepEditing": "แก้ไขต่อ", + "discard": "ทิ้งการเปลี่ยนแปลง", + "faq0Question": "กำหนดปุ่มอย่างไร?", + "faq0Answer": "เลือกกลุ่มและเพิ่มอินพุต แล้วใช้บันทึกปุ่มหรือรายการ หนึ่งการกระทำมีได้หลายอินพุต", + "faq1Question": "วางไฟล์ INI ที่ไหน?", + "faq1Answer": "วาง anyps5-input.ini ข้างไฟล์โปรแกรมเกมที่สร้างขึ้น ใช้ ANYPS5_INPUT_CONFIG สำหรับตำแหน่งอื่น", + "faq2Question": "ทำไมบางปุ่มจึงบันทึกไม่ได้?", + "faq2Answer": "ทางลัดของเบราว์เซอร์หรือระบบอาจดักปุ่มไว้ เลือกจากรายการแทน Escape ยกเลิกการบันทึก จึงต้องกำหนดจากรายการ", + "faq3Question": "บันทึกการเปลี่ยนแปลงอัตโนมัติไหม?", + "faq3Answer": "การเปลี่ยนแปลงอยู่ในหน่วยความจำเท่านั้น คัดลอกหรือดาวน์โหลดก่อนโหลดใหม่ บันทึกเฉพาะภาษา การกระทำที่ยังไม่ตั้งค่าจะใช้ค่าเริ่มต้น", + "searchOptions": "ค้นหาตัวเลือก", + "noOptions": "ไม่พบตัวเลือกที่ตรงกัน" + }, + "sw": { + "controls": "Vidhibiti", + "configuration": "Usanidi", + "help": "Msaada", + "settings": "Mipangilio", + "language": "Lugha", + "close": "Funga", + "eyebrow": "Vifaa / Vidhibiti maalum", + "intro": "Chagua kidhibiti kisha weka ingizo la kibodi au kipanya.", + "search": "Tafuta vidhibiti", + "import": "Ingiza INI", + "reset": "Weka upya", + "group0": "Vibonye vikuu", + "group1": "Vibonye vya juu", + "group2": "Vibonye vya mwelekeo", + "group3": "Kijiti cha kushoto", + "group4": "Kijiti cha kulia", + "group5": "Mguso na zana", + "select": "Chagua…", + "add": "+ Ongeza ingizo", + "capture": "Rekodi", + "remove": "Ondoa", + "builtin": "Chaguo-msingi: {inputs}", + "custom": "Ingizo maalum linachukua nafasi ya ingizo la kawaida.", + "noResults": "Hakuna vidhibiti vilivyopatikana.", + "download": "Pakua INI", + "copy": "Nakili usanidi", + "previewEmpty": "Ongeza ingizo ili kuanza.", + "backControls": "Rudi kwenye vidhibiti", + "switchGroup": "Badili kundi", + "moveFocus": "Hamisha fokasi", + "helpIntro": "Majibu mafupi ya kukusaidia kuanza.", + "configIntro": "Kagua ingizo kisha nakili au pakua faili.", + "captureTitle": "Bonyeza kitufe kwa {action}", + "cancelCapture": "Ghairi kurekodi", + "keepEditing": "Endelea kuhariri", + "discard": "Tupa mabadiliko", + "faq0Question": "Ninawekaje kitufe?", + "faq0Answer": "Chagua kundi na Ongeza ingizo, kisha Rekodi au tumia orodha. Kitendo kimoja kinaweza kuwa na ingizo nyingi.", + "faq1Question": "Ninaweka wapi faili la INI?", + "faq1Answer": "Weka anyps5-input.ini karibu na faili la programu ya mchezo lililozalishwa. Tumia ANYPS5_INPUT_CONFIG kwa njia nyingine.", + "faq2Question": "Kwa nini kitufe hakirekodiwi?", + "faq2Answer": "Njia ya mkato ya kivinjari au mfumo inaweza kukizuia. Chagua kwenye orodha. Escape hughairi kurekodi; kiweke kupitia orodha.", + "faq3Question": "Mabadiliko huhifadhiwa kiotomatiki?", + "faq3Answer": "Mabadiliko hubaki kwenye kumbukumbu: nakili au pakua kabla ya kupakia upya. Lugha pekee huhifadhiwa. Vitendo visivyosanidiwa hutumia chaguo-msingi.", + "searchOptions": "Tafuta chaguo", + "noOptions": "Hakuna chaguo linalolingana." + }, + "nl": { + "controls": "Bediening", + "configuration": "Configuratie", + "help": "Hulp", + "settings": "Instellingen", + "language": "Taal", + "close": "Sluiten", + "eyebrow": "Accessoires / Eigen bediening", + "intro": "Kies een actie en wijs toetsenbord- of muisinvoer toe.", + "search": "Bediening zoeken", + "import": "INI importeren", + "reset": "Herstellen", + "group0": "Actieknoppen", + "group1": "Schouderknoppen", + "group2": "Richtingsknoppen", + "group3": "Linker stick", + "group4": "Rechter stick", + "group5": "Aanraakvlak en hulpmiddelen", + "select": "Selecteren…", + "add": "+ Invoer toevoegen", + "capture": "Vastleggen", + "remove": "Verwijderen", + "builtin": "Standaard: {inputs}", + "custom": "Eigen invoer vervangt de standaardinvoer.", + "noResults": "Geen passende bediening gevonden.", + "download": "INI downloaden", + "copy": "Configuratie kopiëren", + "previewEmpty": "Voeg invoer toe om te beginnen.", + "backControls": "Terug naar bediening", + "switchGroup": "Groep wisselen", + "moveFocus": "Focus verplaatsen", + "helpIntro": "Korte antwoorden om te beginnen.", + "configIntro": "Controleer de invoer en kopieer of download het bestand.", + "captureTitle": "Druk op een toets voor {action}", + "cancelCapture": "Vastleggen annuleren", + "keepEditing": "Verder bewerken", + "discard": "Wijzigingen weggooien", + "faq0Question": "Hoe wijs ik een toets toe?", + "faq0Answer": "Kies een groep en Invoer toevoegen, dan Vastleggen of de lijst. Eén actie kan meerdere invoeren hebben.", + "faq1Question": "Waar plaats ik het INI-bestand?", + "faq1Answer": "Zet anyps5-input.ini naast het gegenereerde spelprogramma. Gebruik ANYPS5_INPUT_CONFIG voor een ander pad.", + "faq2Question": "Waarom wordt een toets niet vastgelegd?", + "faq2Answer": "Een browser- of systeemsneltoets kan hem onderscheppen. Kies uit de lijst. Escape annuleert vastleggen; wijs Escape via de lijst toe.", + "faq3Question": "Worden wijzigingen automatisch opgeslagen?", + "faq3Answer": "Wijzigingen staan alleen in het geheugen: kopieer of download vóór herladen. Alleen de taal wordt opgeslagen. Ongewijzigde acties houden hun standaardwaarden.", + "searchOptions": "Opties zoeken", + "noOptions": "Geen passende opties." + }, + "pl": { + "controls": "Sterowanie", + "configuration": "Konfiguracja", + "help": "Pomoc", + "settings": "Ustawienia", + "language": "Język", + "close": "Zamknij", + "eyebrow": "Akcesoria / Własne sterowanie", + "intro": "Wybierz akcję i przypisz klawisz lub przycisk myszy.", + "search": "Szukaj sterowania", + "import": "Importuj INI", + "reset": "Resetuj", + "group0": "Główne przyciski", + "group1": "Górne przyciski", + "group2": "Przyciski kierunkowe", + "group3": "Lewy drążek", + "group4": "Prawy drążek", + "group5": "Dotyk i narzędzia", + "select": "Wybierz…", + "add": "+ Dodaj wejście", + "capture": "Zarejestruj", + "remove": "Usuń", + "builtin": "Domyślne: {inputs}", + "custom": "Własne wejścia zastępują domyślne.", + "noResults": "Nie znaleziono pasującej akcji.", + "download": "Pobierz INI", + "copy": "Kopiuj konfigurację", + "previewEmpty": "Dodaj wejście, aby zacząć.", + "backControls": "Wróć do sterowania", + "switchGroup": "Zmień grupę", + "moveFocus": "Przenieś fokus", + "helpIntro": "Krótkie odpowiedzi na początek.", + "configIntro": "Sprawdź wejścia i skopiuj lub pobierz plik.", + "captureTitle": "Naciśnij klawisz dla {action}", + "cancelCapture": "Anuluj rejestrację", + "keepEditing": "Edytuj dalej", + "discard": "Odrzuć zmiany", + "faq0Question": "Jak przypisać klawisz?", + "faq0Answer": "Wybierz grupę i Dodaj wejście, potem Zarejestruj lub listę. Jedna akcja może mieć kilka wejść.", + "faq1Question": "Gdzie umieścić plik INI?", + "faq1Answer": "Umieść anyps5-input.ini obok wygenerowanego pliku wykonywalnego gry. Inny adres podaj w ANYPS5_INPUT_CONFIG.", + "faq2Question": "Dlaczego klawisz nie jest rejestrowany?", + "faq2Answer": "Skrót przeglądarki lub systemu może go przechwycić. Wybierz z listy. Escape anuluje rejestrację; przypisz go z listy.", + "faq3Question": "Czy zmiany zapisują się automatycznie?", + "faq3Answer": "Zmiany są tylko w pamięci: skopiuj lub pobierz przed odświeżeniem. Zapisywany jest tylko język. Nieustawione akcje zachowują wartości domyślne.", + "searchOptions": "Szukaj opcji", + "noOptions": "Brak pasujących opcji." + } +}; + +export function translate(locale, key, values = {}) { + const text = messages[locale]?.[key] ?? messages.en[key] ?? key; + return text.replace(/\{(\w+)\}/gu, (match, name) => String(values[name] ?? match)); +} + +export function diagnostic(locale, message) { + if (locale !== 'ar-EG') return message; + return message + .replace('Configuration must be a list of bindings.', 'الإعدادات لازم تكون قائمة روابط أزرار.') + .replace('Unknown or empty action.', 'الحركة مش معروفة أو فاضية.') + .replace('Input type must be KEY, MOUSE or WHEEL.', 'نوع الإدخال لازم يكون KEY أو MOUSE أو WHEEL.') + .replace(/(\w+) cannot use (\w+)\./gu, '$1 مينفعش يستخدم $2.') + .replace(/Unknown or empty (\w+) value\./gu, 'قيمة $1 مش معروفة أو فاضية.') + .replace('This SDL key name cannot be represented in the current INI comment syntax.', 'اسم الزرار ده مينفعش يتحفظ بالصيغة الحالية لملف INI.') + .replace(/duplicate (.+) will be omitted\./gu, 'الربط $1 متكرر وهيتحفظ مرة واحدة.') + .replace(' controls ', ' بيشغّل ') + .replaceAll(' (built-in)', ' (افتراضي)') + .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 من القائمة.'); +} + +export function initialLocale(savedLocale) { + return locales.some(item => item.code === savedLocale) ? savedLocale : 'en'; +} diff --git a/tools/input-mapper/index.html b/tools/input-mapper/index.html new file mode 100644 index 0000000000..3b340b95e6 --- /dev/null +++ b/tools/input-mapper/index.html @@ -0,0 +1,78 @@ + + + + + + + AnyPS5 · Input mapper + + + + + + +
+ AnyPS5 + +
+ + + +
+
+
+ +
+

Accessories / Custom controls

+

Face buttons

+

Choose a control, then assign your keyboard or mouse inputs.

+
+
+ +
+ + + +
+
+
+ + +
+

+ +
+
© Amr Yasser←→ Switch groupTab Move focus
+ +

Press a key

+

Capture uses physical key positions. Escape cancels capture; choose Escape from the list to assign it. Some browser and system shortcuts require manual selection.

+

+ +
+ +

Discard unsaved changes?

+

Copy or download your configuration first if you want to keep it.

+
+ + +
+
+ + +

+ +
+ +
+ + + diff --git a/tools/input-mapper/keys.js b/tools/input-mapper/keys.js new file mode 100644 index 0000000000..5c44d49036 --- /dev/null +++ b/tools/input-mapper/keys.js @@ -0,0 +1,230 @@ +export const keyNames = [ + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "0", + "Return", + "Escape", + "Backspace", + "Tab", + "Space", + "-", + "=", + "[", + "]", + "\\", + "#", + ";", + "'", + "`", + ",", + ".", + "/", + "CapsLock", + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12", + "PrintScreen", + "ScrollLock", + "Pause", + "Insert", + "Home", + "PageUp", + "Delete", + "End", + "PageDown", + "Right", + "Left", + "Down", + "Up", + "Numlock", + "Keypad /", + "Keypad *", + "Keypad -", + "Keypad +", + "Keypad Enter", + "Keypad 1", + "Keypad 2", + "Keypad 3", + "Keypad 4", + "Keypad 5", + "Keypad 6", + "Keypad 7", + "Keypad 8", + "Keypad 9", + "Keypad 0", + "Keypad .", + "Application", + "Power", + "Keypad =", + "F13", + "F14", + "F15", + "F16", + "F17", + "F18", + "F19", + "F20", + "F21", + "F22", + "F23", + "F24", + "Execute", + "Help", + "Menu", + "Select", + "Stop", + "Again", + "Undo", + "Cut", + "Copy", + "Paste", + "Find", + "Mute", + "VolumeUp", + "VolumeDown", + "Keypad ,", + "Keypad = (AS400)", + "AltErase", + "SysReq", + "Cancel", + "Clear", + "Prior", + "Separator", + "Out", + "Oper", + "Clear / Again", + "CrSel", + "ExSel", + "Keypad 00", + "Keypad 000", + "ThousandsSeparator", + "DecimalSeparator", + "CurrencyUnit", + "CurrencySubUnit", + "Keypad (", + "Keypad )", + "Keypad {", + "Keypad }", + "Keypad Tab", + "Keypad Backspace", + "Keypad A", + "Keypad B", + "Keypad C", + "Keypad D", + "Keypad E", + "Keypad F", + "Keypad XOR", + "Keypad ^", + "Keypad %", + "Keypad <", + "Keypad >", + "Keypad &", + "Keypad &&", + "Keypad |", + "Keypad ||", + "Keypad :", + "Keypad #", + "Keypad Space", + "Keypad @", + "Keypad !", + "Keypad MemStore", + "Keypad MemRecall", + "Keypad MemClear", + "Keypad MemAdd", + "Keypad MemSubtract", + "Keypad MemMultiply", + "Keypad MemDivide", + "Keypad +/-", + "Keypad Clear", + "Keypad ClearEntry", + "Keypad Binary", + "Keypad Octal", + "Keypad Decimal", + "Keypad Hexadecimal", + "Left Ctrl", + "Left Shift", + "Left Alt", + "Left GUI", + "Right Ctrl", + "Right Shift", + "Right Alt", + "Right GUI", + "ModeSwitch", + "AudioNext", + "AudioPrev", + "AudioStop", + "AudioPlay", + "AudioMute", + "MediaSelect", + "WWW", + "Mail", + "Calculator", + "Computer", + "AC Search", + "AC Home", + "AC Back", + "AC Forward", + "AC Stop", + "AC Refresh", + "AC Bookmarks", + "BrightnessDown", + "BrightnessUp", + "DisplaySwitch", + "KBDIllumToggle", + "KBDIllumDown", + "KBDIllumUp", + "Eject", + "Sleep", + "App1", + "App2", + "AudioRewind", + "AudioFastForward", + "SoftLeft", + "SoftRight", + "Call", + "EndCall" +]; + +export const exportableKeys = keyNames.filter(name => !/[#;]/u.test(name)); diff --git a/tools/input-mapper/mapping.js b/tools/input-mapper/mapping.js new file mode 100644 index 0000000000..fe476f10f5 --- /dev/null +++ b/tools/input-mapper/mapping.js @@ -0,0 +1,35 @@ +export const groups = [ + {name: 'Face buttons', actions: ['Cross', 'Circle', 'Triangle', 'Square']}, + {name: 'Shoulders, triggers and stick clicks', actions: ['L1', 'R1', 'L2', 'R2', 'L3', 'R3']}, + {name: 'Directional controls', actions: ['Up', 'Right', 'Down', 'Left']}, + {name: 'Left analog stick', actions: ['LeftStickLeft', 'LeftStickRight', 'LeftStickUp', 'LeftStickDown']}, + {name: 'Right analog stick', actions: ['RightStickLeft', 'RightStickRight', 'RightStickUp', 'RightStickDown']}, + {name: 'Touch and utility', actions: ['Options', 'TouchLeft', 'TouchRight', 'StickModifier', 'Shake', 'ToggleMouse', 'ToggleFullscreen', 'TogglePerformanceOverlay', 'Screenshot']} +]; + +export const actions = groups.flatMap(group => group.actions); +export const padButtons = new Set(['Cross', 'Circle', 'Triangle', 'Square', 'L1', 'R1', 'L2', 'R2', 'L3', 'R3', 'Options', 'Up', 'Right', 'Down', 'Left']); +export const mouseValues = ['Left', 'Middle', 'Right', 'X1', 'X2']; +export const wheelValues = ['Up', 'Down']; + +export const defaults = { + Cross: ['KEY:Return', 'KEY:Space'], Circle: ['KEY:C'], Triangle: ['KEY:I'], Square: ['MOUSE:Left'], + L1: ['KEY:Q'], R1: ['KEY:E', 'KEY:Left Alt', 'KEY:Right Alt'], L2: ['KEY:R', 'MOUSE:X1'], R2: ['MOUSE:Right'], + L3: ['KEY:Left Shift', 'KEY:Right Shift'], R3: ['KEY:Left Ctrl', 'KEY:Right Ctrl'], Options: ['KEY:Escape'], + Up: ['KEY:Up', 'WHEEL:Up'], Right: ['KEY:Right'], Down: ['KEY:Down', 'WHEEL:Down'], Left: ['KEY:Left'], + LeftStickLeft: ['KEY:A'], LeftStickRight: ['KEY:D'], LeftStickUp: ['KEY:W'], LeftStickDown: ['KEY:S'], + RightStickLeft: ['KEY:F'], RightStickRight: ['KEY:H'], RightStickUp: ['KEY:T'], RightStickDown: ['KEY:G'], + TouchLeft: ['KEY:Backspace'], TouchRight: ['KEY:Tab'], StickModifier: [], Shake: ['KEY:X'], + ToggleMouse: ['KEY:F7', 'MOUSE:Middle'], ToggleFullscreen: ['KEY:F11'], TogglePerformanceOverlay: ['KEY:F10'], Screenshot: ['KEY:F12'] +}; + +export function typesForAction(action) { + if (!actions.includes(action)) return []; + if (['ToggleFullscreen', 'TogglePerformanceOverlay', 'Screenshot'].includes(action)) return ['KEY']; + return padButtons.has(action) ? ['KEY', 'MOUSE', 'WHEEL'] : ['KEY', 'MOUSE']; +} + +export function canonicalValue(value, choices) { + if (typeof value !== 'string') return undefined; + return choices.find(choice => choice.toUpperCase() === value.trim().toUpperCase()); +} diff --git a/tools/input-mapper/package.json b/tools/input-mapper/package.json new file mode 100644 index 0000000000..064ea01892 --- /dev/null +++ b/tools/input-mapper/package.json @@ -0,0 +1,15 @@ +{ + "name": "anyps5-input-mapper", + "version": "1.0.0", + "private": true, + "description": "Browser-based keyboard and mouse input configurator for AnyPS5", + "type": "module", + "license": "GPL-2.0", + "engines": { + "node": ">=22" + }, + "scripts": { + "start": "node serve.js", + "test": "node --test tests/*.test.js" + } +} diff --git a/tools/input-mapper/serve.js b/tools/input-mapper/serve.js new file mode 100644 index 0000000000..ccdb20e26c --- /dev/null +++ b/tools/input-mapper/serve.js @@ -0,0 +1,30 @@ +import {createServer} from 'node:http'; +import {readFile} from 'node:fs/promises'; + +const files = new Set(['index.html', 'styles.css', 'app.js', 'mapping.js', 'keys.js', 'capture.js', 'validation.js', 'i18n.js', 'dropdown.js']); +const mime = {html: 'text/html', css: 'text/css', js: 'text/javascript'}; +const port = Number(process.env.PORT || 4173); +const server = createServer(async (request, response) => { + const path = new URL(request.url, 'http://127.0.0.1').pathname; + const file = path === '/' ? 'index.html' : path.slice(1); + if (!['GET', 'HEAD'].includes(request.method)) { + response.writeHead(405, {Allow: 'GET, HEAD'}).end(); + return; + } + if (!files.has(file)) { + response.writeHead(404).end('Not found'); + return; + } + try { + const content = await readFile(new URL(file, import.meta.url)); + response.writeHead(200, {'Content-Type': `${mime[file.split('.').pop()]}; charset=utf-8`, 'Cache-Control': 'no-store'}); + response.end(request.method === 'HEAD' ? undefined : content); + } catch { + response.writeHead(500).end('Cannot read static file'); + } +}); +server.on('error', error => { + console.error(error.message); + process.exitCode = 1; +}); +server.listen(port, '127.0.0.1', () => console.log(`Input mapper: http://127.0.0.1:${port}`)); diff --git a/tools/input-mapper/styles.css b/tools/input-mapper/styles.css new file mode 100644 index 0000000000..1be7b42096 --- /dev/null +++ b/tools/input-mapper/styles.css @@ -0,0 +1,203 @@ +:root { color-scheme: dark; font-family: "Segoe UI", Helvetica, Arial, sans-serif; font-weight: 400; color: #f4f5f7; background: #101116; --muted: #c1c4ce; --line: #ffffff1c; --ease: cubic-bezier(.2,.75,.25,1); } +* { box-sizing: border-box; } +* { scrollbar-width: thin; scrollbar-color: #dbe0ea70 transparent; } +::-webkit-scrollbar { width: 4px; height: 4px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: #dbe0ea70; border-radius: 20px; } +::-webkit-scrollbar-thumb:hover { background: #eef1f8c0; } +::-webkit-scrollbar-button { display: none; width: 0; height: 0; } +::-webkit-scrollbar-corner { background: transparent; } +@supports selector(::-webkit-scrollbar) { * { scrollbar-width: auto; scrollbar-color: auto; } } +[hidden] { display: none !important; } +html { scroll-behavior: smooth; scroll-padding-top: 30px; } +body { margin: 0; min-height: 100vh; isolation: isolate; } +a { color: inherit; } +button, input, select, textarea { font: inherit; } +button, input, select { min-height: 42px; border: 1px solid #ffffff24; border-radius: 4px; color: inherit; background: #ffffff09; padding: 9px 14px; transition: color .2s, background .2s, border-color .2s, box-shadow .25s, transform .25s var(--ease); } +button { cursor: pointer; font-weight: 400; } +button:hover:not(:disabled) { background: #ffffff16; border-color: #ffffff70; } +button:active:not(:disabled) { transform: scale(.97); } +button:disabled { cursor: not-allowed; opacity: .42; } +:focus-visible { outline: none; box-shadow: inset 0 -2px 0 #7d9ac9; } +button:focus-visible:not(:disabled) { background: #31415b80; } +input:focus-visible, textarea:focus-visible { border-color: #788da9; } +select option { color: #f4f5f7; background: #22232a; } +.ambient { position: fixed; inset: 0; z-index: -1; overflow: hidden; pointer-events: none; background: radial-gradient(ellipse at 80% 100%, #16385278, transparent 65%), radial-gradient(ellipse at 20% 18%, #4c484b50, transparent 60%), linear-gradient(140deg, #18191e, #101116 50%, #111820); } +.ambient-glow { position: absolute; width: 110%; height: 75%; top: -45%; left: -20%; border-radius: 50%; background: radial-gradient(ellipse, #c9b99a33, #b3bfd614 38%, transparent 68%); filter: blur(36px); animation: atmosphere 24s ease-in-out infinite alternate; } +.particles { position: absolute; inset: -12%; opacity: .7; background-image: radial-gradient(circle at 12% 29%, #e3d5b5d0 0 1px, transparent 2px), radial-gradient(circle at 32% 68%, #e0d5b9a0 0 1px, transparent 3px), radial-gradient(circle at 68% 23%, #e8d8b6b0 0 1px, transparent 3px), radial-gradient(circle at 88% 59%, #f4ead2a0 0 1px, transparent 2px), radial-gradient(circle at 54% 48%, #a2bfda90 0 1px, transparent 2px), radial-gradient(circle at 76% 78%, #ebd9ada0 0 1px, transparent 3px); background-size: 410px 370px, 590px 530px, 670px 490px, 710px 630px, 830px 730px, 970px 810px; animation: particles 38s linear infinite alternate; } +.particles.distant { opacity: .25; filter: blur(1px); animation-duration: 58s; animation-direction: alternate-reverse; } +header { max-width: 1640px; margin: auto; display: flex; align-items: center; gap: 42px; padding: 34px 5.5%; } +.brand { font-size: 20px; text-decoration: none; font-weight: 300; letter-spacing: -.5px; } +.system-nav { display: flex; gap: 32px; } +.system-nav a { text-decoration: none; font-size: 23px; font-weight: 300; color: #b8bac1; transition: color .2s; } +.system-nav .current, .system-nav a:hover { color: white; } +.system-tools { display: flex; align-items: center; gap: 24px; margin-inline-start: auto; } +.icon-button { display: grid; place-items: center; width: 38px; min-height: 38px; border: 0; border-radius: 50%; padding: 0; background: transparent; text-decoration: none; font-size: 26px; } +.search-icon { width: 15px; height: 15px; border: 2px solid currentColor; border-radius: 50%; position: relative; } +.search-icon::after { content: ''; position: absolute; height: 7px; width: 2px; background: currentColor; right: -4px; bottom: -5px; transform: rotate(-45deg); } +.translation-icon { display: flex; align-items: center; gap: 2px; direction: ltr; font-size: 18px; line-height: 1; } +.translation-icon span { display: grid; place-items: center; width: 21px; height: 23px; border: 1px solid #aab7cb70; border-radius: 4px; } +.translation-icon span:last-child { margin-left: -7px; margin-top: 11px; background: #252a34; } +#clock { font-size: 22px; font-weight: 300; min-width: 58px; font-variant-numeric: tabular-nums; white-space: nowrap; } +main { max-width: 1440px; min-height: calc(100vh - 210px); margin: auto; padding: 6px 5% 35px; } +main:focus { outline: none; box-shadow: none; } +#categories { display: flex; align-items: flex-start; gap: 20px; padding: 16px 8px 22px; margin-inline-start: -8px; min-height: 155px; overflow-x: auto; } +.category { background: transparent; border: 0; padding: 0; width: 108px; flex: 0 0 108px; text-align: center; } +.category:hover:not(:disabled), .category:focus-visible:not(:disabled) { background: transparent; color: white; } +.category:focus-visible { outline: none; box-shadow: none; } +.category-icon { display: grid; place-items: center; height: 92px; width: 92px; margin: 0 auto 13px; border-radius: 16px; border: 1px solid #ffffff24; background: linear-gradient(145deg, #565860a0, #272b33c0); color: #eeeeef; box-shadow: 0 8px 20px #0002; transition: transform .3s var(--ease), box-shadow .3s, background .3s; } +.category[aria-pressed="true"] .category-icon, .category:focus-visible .category-icon { transform: scale(1.06); background: linear-gradient(145deg, #6a778db0, #354359c0); border-color: #91a4c366; box-shadow: 0 12px 30px #0004; } +.category:hover .category-icon { transform: translateY(-3px); } +.category[aria-pressed="true"]:hover .category-icon { transform: scale(1.06) translateY(-2px); } +.category-label { display: block; font-size: 12px; color: #babdc6; line-height: 1.4; } +.category[aria-pressed="true"] .category-label { color: white; } +.face-icon { position: relative; width: 60px; height: 60px; font-size: 24px; line-height: 1; font-weight: 300; } +.face-icon span { position: absolute; display: grid; place-items: center; width: 24px; height: 24px; } +.face-icon span:nth-child(1) { top: 0; left: 18px; } +.face-icon span:nth-child(2) { right: 0; top: 18px; } +.face-icon span:nth-child(3) { bottom: 0; left: 18px; } +.face-icon span:nth-child(4) { left: 0; top: 18px; } +.shoulder-icon { border: 2px solid currentColor; border-radius: 6px 6px 13px 13px; padding: 7px 10px; font-size: 17px; } +.direction-icon { position: relative; width: 42px; height: 42px; background: currentColor; clip-path: polygon(34% 0,66% 0,66% 34%,100% 34%,100% 66%,66% 66%,66% 100%,34% 100%,34% 66%,0 66%,0 34%,34% 34%); } +.stick-icon { display: grid; place-items: center; border: 2px solid currentColor; border-radius: 50%; width: 43px; height: 43px; font-size: 18px; box-shadow: 0 4px 0 1px #ffffff45; } +.touch-icon { width: 50px; height: 33px; border: 2px solid currentColor; border-radius: 7px; background: radial-gradient(circle, #ffffff60 1px, transparent 1.5px) 0 0 / 7px 7px; } +.intro { margin: 8px 0 26px; animation: enter .6s var(--ease) both; } +.eyebrow { color: #b9bbc2; font-size: 13px; margin: 0 0 13px; } +h1 { font-size: clamp(34px, 4vw, 48px); font-weight: 300; letter-spacing: -.9px; line-height: 1.15; margin: 0; } +.intro > p:last-child { color: var(--muted); font-size: 15px; margin: 13px 0 0; } +.toolbar, .tools, .output-heading { display: flex; gap: 12px; align-items: center; } +.toolbar { justify-content: space-between; margin-bottom: 22px; } +.search { width: min(350px, 55%); } +.search input { width: 100%; border-radius: 30px; padding: 10px 20px; background: #ffffff08; } +input::placeholder { color: #b6b9c4; } +.tools button { border-radius: 30px; padding: 9px 22px; } +.group, .configuration-panel, .faq { background: #181a22b0; border: 1px solid var(--line); border-radius: 6px; backdrop-filter: blur(20px); box-shadow: 0 18px 45px #0002; } +.group { margin-bottom: 20px; animation: enter .35s var(--ease); } +.group h2 { font-size: 12px; font-weight: 400; color: #bdc2ce; margin: 0; padding: 18px 24px; border-bottom: 1px solid var(--line); } +.action { padding: 19px 24px; border-bottom: 1px solid #ffffff10; transition: background .25s; } +.action:last-child { border-bottom: 0; } +.action:hover { background: #ffffff06; } +.action:focus-within { background: #ffffff0c; } +.action-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; } +.action-title { display: flex; align-items: center; gap: 15px; } +.action-symbol { display: grid; place-items: center; width: 30px; height: 30px; font-size: 29px; line-height: 1; font-weight: 300; color: #eaedf5; } +.action h3 { margin: 0; font-size: 18px; font-weight: 300; } +.action-heading button { border: 0; min-height: 34px; font-size: 12px; border-radius: 30px; padding: 7px 14px; background: #ffffff0a; } +.hint { font-size: 12px; color: var(--muted); line-height: 1.65; overflow-wrap: anywhere; } +.action > .hint { margin: 7px 0 0; margin-inline-start: 45px; } +.binding { display: grid; grid-template-columns: 110px minmax(0, 1fr) auto auto; gap: 10px; margin-top: 17px; animation: enter .25s var(--ease); } +.dropdown { min-width: 0; } +.dropdown-trigger { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 44px; padding: 10px 13px; font-size: 13px; text-align: start; border-radius: 9px; background: #0e131e66; } +.dropdown-trigger::after { content: ''; width: 6px; height: 6px; flex-shrink: 0; border-right: 1.5px solid #a7b2c5; border-bottom: 1.5px solid #a7b2c5; transform: rotate(45deg); margin-inline-start: auto; margin-top: -4px; } +.dropdown-value { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.dropdown-trigger[aria-expanded="true"] { background: #27344999; border-color: #677c9a; } +.binding button { font-size: 12px; padding: 8px 11px; } +[aria-invalid="true"] { border-color: #ff9a9a; } +.configuration-panel { padding: 32px; max-width: 960px; margin: auto; animation: enter .35s var(--ease); } +.output-heading { justify-content: space-between; } +.output-heading h2 { font-size: 22px; font-weight: 300; margin: 0; } +.tag { font-size: 11px; color: #b6bcc9; white-space: nowrap; } +.configuration-panel label { display: block; margin: 20px 0 10px; color: #d0d3db; font-size: 12px; direction: ltr; } +textarea { width: 100%; height: 340px; resize: vertical; padding: 18px; border: 1px solid #ffffff18; border-radius: 4px; background: #090b1290; color: #edf0f8; font: 14px/1.9 Consolas, ui-monospace, monospace; text-align: left; } +.export-actions { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin-top: 20px; } +.export-actions button { border-radius: 30px; } +.primary { background: #f0f1f5; color: #171920; border-color: #f0f1f5; } +.primary:hover:not(:disabled), .primary:focus-visible:not(:disabled) { background: #d8e0ee; color: #171920; box-shadow: none; } +.error { color: #ffb5b5; } +.warning { color: #e8cf9b; } +#validation { font-size: 12px; line-height: 1.6; } +#validation ul { padding-inline-start: 18px; } +#count { direction: ltr; unicode-bidi: isolate; } +#help { max-width: 960px; margin: auto; } +.faq { margin-bottom: 14px; } +.faq summary { cursor: pointer; padding: 22px 26px; font-size: 18px; font-weight: 300; } +.faq[open] summary { border-bottom: 1px solid var(--line); } +.faq p { padding: 0 26px 12px; color: var(--muted); line-height: 1.9; } +#picker-dialog { position: fixed; inset: auto; margin: 0; padding: 18px; border: 1px solid #70809c45; border-radius: 16px; background: linear-gradient(145deg, #242a36fa, #131a27fc); max-width: calc(100% - 32px); height: min(var(--picker-height, 480px), calc(100dvh - 32px)); max-height: calc(100dvh - 32px); box-shadow: 0 24px 80px #0008; } +#picker-dialog[open] { display: grid; grid-template-rows: auto auto minmax(0, 1fr); gap: 14px; } +#picker-dialog::backdrop { background: #080b1540; backdrop-filter: blur(3px); } +.picker-heading { display: flex; gap: 12px; align-items: center; justify-content: space-between; min-width: 0; } +.picker-heading h2 { font-size: 17px; line-height: 1.5; overflow-wrap: anywhere; } +.picker-heading button { min-height: 36px; margin: 0; padding: 6px 12px; border: 0; background: #ffffff08; font-size: 12px; flex-shrink: 0; } +#picker-search { width: 100%; min-width: 0; border-radius: 10px; padding: 11px 14px; font-size: 16px; } +#picker-options { min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 2px 3px; } +.picker-option { display: block; width: 100%; min-height: 44px; margin: 0 0 3px; border: 0; border-radius: 8px; padding: 12px 14px; background: transparent; text-align: start; font-size: 14px; line-height: 1.5; overflow-wrap: anywhere; } +.picker-option[aria-selected="true"] { background: #3547629c; color: #e4edff; } +.picker-option:focus-visible { background: #3b506fa6; box-shadow: none; } +#picker-empty { position: absolute; top: 160px; inset-inline: 18px; text-align: center; } +html[dir="rtl"] h1 { letter-spacing: 0; } +html[dir="rtl"] .intro > p:last-child { line-height: 1.9; } +html[dir="rtl"] .brand, footer > span:first-child { direction: ltr; unicode-bidi: isolate; } +#status { color: #dae3f6; font-size: 13px; line-height: 1.6; } +#import-report { margin-top: 22px; padding: 24px; border: 1px solid var(--line); background: #171b25c0; border-radius: 6px; overflow-wrap: anywhere; } +#import-report h2 { font-size: 23px; font-weight: 300; } +dialog { width: min(520px, calc(100% - 32px)); max-height: calc(100dvh - 32px); overflow-y: auto; color: inherit; background: linear-gradient(140deg, #30323bef, #181d28f5); border: 1px solid #ffffff3c; border-radius: 6px; padding: 32px; box-shadow: 0 20px 100px #0008; } +dialog[open] { animation: dialog-enter .3s var(--ease); } +dialog h2 { font-size: 28px; font-weight: 300; margin: 0; } +dialog p { line-height: 1.7; font-size: 14px; color: var(--muted); } +dialog button { border-radius: 30px; margin-top: 12px; } +dialog form { display: flex; gap: 12px; flex-wrap: wrap; } +dialog::backdrop { background: #080b15a0; backdrop-filter: blur(12px); } +footer { display: flex; justify-content: space-between; align-items: center; gap: 18px; padding: 20px 5.5% 30px; max-width: 1640px; margin: auto; color: #b1b6c2; font-size: 12px; } +.navigation-hint { display: flex; gap: 7px; align-items: center; } +kbd { border: 1px solid #ffffff50; border-radius: 4px; padding: 1px 5px; font: inherit; color: #e3e5eb; } +.footer-divider { width: 1px; height: 12px; background: #ffffff30; margin: 0 8px; } +.skip-link { position: fixed; top: -80px; left: 16px; padding: 12px; z-index: 2; background: #242836; } +.skip-link:focus { top: 12px; } +.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; } +@keyframes atmosphere { to { transform: translate(15%, 20%) scale(1.15); opacity: .7; } } +@keyframes particles { from { transform: translate3d(0, 0, 0); } to { transform: translate3d(-35px, -65px, 0); } } +@keyframes enter { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } +@keyframes dialog-enter { from { opacity: 0; transform: translateY(16px) scale(.98); } to { opacity: 1; transform: translateY(0) scale(1); } } +@media (max-width: 1100px) { header { gap: 26px; } #categories { gap: 14px; } .category { width: 96px; flex-basis: 96px; } .category-icon { width: 82px; height: 82px; } } +@media (max-width: 760px) { + header { display: grid; grid-template-columns: 1fr auto; gap: 18px 12px; padding: 22px 5% 14px; } + .brand { font-size: 18px; } + .system-tools { margin: 0; gap: 12px; } + .system-nav { grid-column: 1 / -1; grid-row: 2; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 3px; background: #ffffff04; border-radius: 12px; padding: 4px; } + .system-nav a { min-width: 0; padding: 10px 6px; font-size: clamp(12px, 3.7vw, 16px); text-align: center; border-radius: 8px; overflow-wrap: anywhere; } + .system-nav .current { background: #3b4b6233; } + #clock { font-size: 15px; } + main { min-height: calc(100vh - 225px); padding-inline: 5%; } + #categories { gap: 10px; padding: 12px 8px 16px; min-height: 119px; scroll-snap-type: x proximity; } + .category { flex-basis: 80px; width: 80px; scroll-snap-align: center; } + .category-icon { width: 66px; height: 66px; border-radius: 13px; margin-bottom: 10px; } + .category-label { font-size: 10px; min-height: 28px; } + .face-icon { transform: scale(.8); } + .intro { margin: 9px 0 22px; } + h1 { font-size: clamp(28px, 7.5vw, 36px); } + .intro > p:last-child { font-size: 13px; line-height: 1.7; } + .eyebrow { font-size: 11px; margin-bottom: 10px; } + .toolbar { flex-wrap: wrap; gap: 10px; } + .search, .tools { width: 100%; } + .search input { font-size: 16px; min-height: 44px; } + .tools button { flex: 1; min-height: 44px; padding: 10px 14px; font-size: 13px; } + .group h2 { padding: 15px 16px; font-size: 11px; } + .action { padding: 17px 16px; } + .action-heading { flex-wrap: wrap; gap: 10px; } + .action-title { gap: 10px; min-width: 0; flex: 1 1 140px; } + .action h3 { font-size: 16px; overflow-wrap: anywhere; } + .action-heading button { min-height: 38px; max-width: 100%; } + .action-symbol { width: 25px; flex-shrink: 0; } + .action > .hint { margin-inline-start: 0; font-size: 11px; } + .binding { grid-template-columns: 104px minmax(0, 1fr); gap: 10px; margin-top: 14px; } + .binding > button { min-height: 44px; border-radius: 9px; } + .dropdown-trigger { font-size: 14px; } + .configuration-panel { padding: 18px; } + textarea { height: clamp(220px, 40dvh, 340px); font-size: 13px; } + .export-actions { grid-template-columns: 1fr; gap: 10px; } + .export-actions button { min-height: 44px; } + .faq summary { padding: 18px; font-size: 16px; } + .faq p { padding-inline: 18px; font-size: 14px; } + dialog { padding: 22px; border-radius: 16px; } + dialog h2 { font-size: 23px; } + footer { justify-content: center; padding-block: 22px; } + .navigation-hint { display: none; } +} +@media (max-width: 600px) { + #picker-dialog { inset-inline: 8px; top: auto; bottom: 8px; width: calc(100% - 16px); max-width: calc(100% - 16px); height: min(var(--picker-height, 480px), 76dvh); max-height: calc(100dvh - 16px); padding: 18px 16px calc(16px + env(safe-area-inset-bottom)); border-radius: 22px; } + #picker-dialog::backdrop { background: #05081080; backdrop-filter: blur(7px); } + .picker-option { min-height: 46px; font-size: 15px; } +} +@media (max-width: 360px) { #clock { display: none; } .system-tools { gap: 16px; } .binding { grid-template-columns: 94px minmax(0, 1fr); } .action-title { flex-basis: 120px; } } +@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } *, *::before, *::after { animation: none !important; transition: none !important; } } diff --git a/tools/input-mapper/tests/i18n.test.js b/tools/input-mapper/tests/i18n.test.js new file mode 100644 index 0000000000..65d98f0140 --- /dev/null +++ b/tools/input-mapper/tests/i18n.test.js @@ -0,0 +1,39 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {diagnostic, initialLocale, locales, messages, translate} from '../i18n.js'; +import {parseIni, serialize} from '../validation.js'; + +test('every selectable language supplies the main interface and every FAQ', () => { + assert.equal(locales[0].code, 'ar-EG'); + assert.equal(new Set(locales.map(item => item.code)).size, locales.length); + const required = ['controls', 'configuration', 'help', 'settings', 'language', 'close', 'intro', 'search', 'import', 'reset', 'select', 'add', 'capture', 'remove', 'download', 'copy', 'backControls', 'helpIntro', 'configIntro']; + for (let index = 0; index < 6; index++) required.push(`group${index}`); + for (let index = 0; index < 4; index++) required.push(`faq${index}Question`, `faq${index}Answer`); + for (const locale of locales) { + assert.doesNotThrow(() => new Intl.DateTimeFormat(locale.code)); + assert.equal(locale.direction, ['ar-EG', 'ur', 'fa'].includes(locale.code) ? 'rtl' : 'ltr'); + for (const key of required) assert.ok(messages[locale.code][key], `${locale.code}: ${key}`); + } +}); + +test('first visits use English while valid saved language preferences are retained', () => { + for (const saved of [undefined, null, '', 'unsupported']) assert.equal(initialLocale(saved), 'en'); + for (const locale of locales) assert.equal(initialLocale(locale.code), locale.code); +}); + +test('language substitution preserves canonical inputs and exported INI bytes', () => { + const source = 'Cross = KEY:F\nCross = KEY:Space\nToggleMouse = MOUSE:X1\n'; + const entries = parseIni(source).entries; + for (const locale of locales) { + assert.ok(translate(locale.code, 'builtin', {inputs: 'KEY:F, KEY:Space'}).includes('KEY:F, KEY:Space')); + assert.equal(serialize(entries), source); + } + assert.equal(translate('ar-EG', 'captured', {value: 'F', action: 'Cross'}), 'تم تسجيل F لـ Cross.'); + assert.equal(translate('unsupported', 'help'), 'Help'); +}); + +test('Egyptian diagnostics retain action tokens and unknown technical details', () => { + assert.equal(diagnostic('ar-EG', 'Cross: Unknown or empty KEY value.'), 'Cross: قيمة KEY مش معروفة أو فاضية.'); + assert.equal(diagnostic('ar-EG', 'Unexpected diagnostic'), 'Unexpected diagnostic'); + assert.equal(diagnostic('fr', 'Cross: Unknown or empty KEY value.'), 'Cross: Unknown or empty KEY value.'); +}); diff --git a/tools/input-mapper/tests/mapping.test.js b/tools/input-mapper/tests/mapping.test.js new file mode 100644 index 0000000000..8c34cceb70 --- /dev/null +++ b/tools/input-mapper/tests/mapping.test.js @@ -0,0 +1,154 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFileSync} from 'node:fs'; +import {actions, defaults, mouseValues, padButtons, typesForAction} from '../mapping.js'; +import {exportableKeys, keyNames} from '../keys.js'; +import {captureKey} from '../capture.js'; +import {parseIni, serialize, validate} from '../validation.js'; + +const binding = (action, type = 'KEY', value = 'F') => ({action, type, value}); + +test('action names and pad button categories agree with the current native parser', () => { + const source = readFileSync(new URL('../../../core/libs/prx/libSceVideoOut/src/InputMapping.cpp', import.meta.url), 'utf8'); + const definitions = [...source.matchAll(/InputAction\{"(\w+)", Pad::InputControl::(\w+),/gu)]; + assert.deepEqual([...actions].sort(), definitions.map(match => match[1]).sort()); + assert.deepEqual([...padButtons].sort(), definitions.filter(match => match[2] === 'Button').map(match => match[1]).sort()); + assert.match(source, /SDL_GetScancodeFromName\(keyName\.c_str\(\)\)/u); + const keyboardOnly = [...source.matchAll(/if \(action\.control == Pad::InputControl::(\w+)\) \{\s*throw std::runtime_error\("\w+ can only use a keyboard key"\);/gu)].map(match => match[1]); + assert.deepEqual(actions.filter(action => typesForAction(action).length === 1).sort(), keyboardOnly.sort()); + assert.match(source, /if \(action\.control != Pad::InputControl::Button\)/u); +}); + +test('built-in conflict catalogue agrees with the current native bindings', () => { + const source = readFileSync(new URL('../../../core/libs/prx/libScePad/include/InputMapping.hpp', import.meta.url), 'utf8'); + const special = {RETURN: 'Return', SPACE: 'Space', ESCAPE: 'Escape', BACKSPACE: 'Backspace', TAB: 'Tab', UP: 'Up', RIGHT: 'Right', DOWN: 'Down', LEFT: 'Left', LALT: 'Left Alt', RALT: 'Right Alt', LSHIFT: 'Left Shift', RSHIFT: 'Right Shift', LCTRL: 'Left Ctrl', RCTRL: 'Right Ctrl'}; + const native = Object.fromEntries(actions.map(action => [action, []])); + for (const match of source.matchAll(/InputBinding\{SDL_SCANCODE_(\w+), MouseButton::(\w+), InputControl::(\w+)(?:, PadButton::(\w+))?(?:, (-?\d+))?\}/gu)) { + const [, key, mouse, control, button, wheel] = match; + const action = control === 'Button' ? button : control; + const value = key !== 'UNKNOWN' ? `KEY:${special[key] || key}` : mouse !== 'None' ? `MOUSE:${mouse}` : `WHEEL:${wheel === '1' ? 'Up' : 'Down'}`; + native[action].push(value); + } + assert.deepEqual(native, defaults); +}); + +test('every action serializes with a supported key', () => { + const input = actions.map(action => binding(action)); + assert.equal(serialize(input), actions.map(action => `${action} = KEY:F\n`).join('')); +}); + +test('multiple inputs are alternatives and omitted actions stay omitted', () => { + assert.equal(serialize([binding('Cross'), binding('Cross', 'KEY', 'Space')]), 'Cross = KEY:F\nCross = KEY:Space\n'); + assert.equal(serialize([]), ''); + assert.equal(serialize([binding('R2', 'MOUSE', 'Right')]), 'R2 = MOUSE:Right\n'); +}); + +test('case and whitespace normalize; duplicate bindings warn and serialize once', () => { + const input = [binding(' cross ', ' key ', ' f '), binding('Cross')]; + assert.equal(serialize(input), 'Cross = KEY:F\n'); + assert.equal(validate(input).warnings.filter(warning => warning.message.includes('duplicate')).length, 1); +}); + +test('invalid and incomplete bindings block export', () => { + for (const input of [binding('Unknown'), binding('Cross', 'GAMEPAD'), binding('Cross', 'KEY', 'Enter'), binding('Cross', 'KEY', 'KeyF'), binding('Cross', 'KEY', ''), binding('Cross', 'KEY', 'F\nR2 = MOUSE:Left'), null, {}]) { + assert.ok(validate([input]).errors.length); + assert.throws(() => serialize([input])); + } + assert.ok(validate({}).errors.length); +}); + +test('all exportable SDL names validate and unsafe comment names are rejected', () => { + for (const key of exportableKeys) assert.equal(validate([binding('Cross', 'KEY', key)]).errors.length, 0, key); + for (const key of [';', '#', 'Keypad #']) assert.ok(validate([binding('Cross', 'KEY', key)]).errors.length, key); + assert.equal(new Set(keyNames).size, keyNames.length); +}); + +test('mouse values and action restrictions match the current INI format', () => { + for (const action of actions) { + for (const value of mouseValues) { + assert.equal(validate([binding(action, 'MOUSE', value)]).errors.length === 0, !['ToggleFullscreen', 'TogglePerformanceOverlay', 'Screenshot'].includes(action)); + } + } + assert.ok(validate([binding('Cross', 'MOUSE', 'Button1')]).errors.length); +}); + +test('wheel directions only map to pad buttons', () => { + for (const action of actions) { + for (const value of ['Up', 'Down']) { + assert.equal(validate([binding(action, 'WHEEL', value)]).errors.length === 0, padButtons.has(action)); + } + assert.deepEqual(typesForAction(action).includes('WHEEL'), padButtons.has(action)); + } + assert.ok(validate([binding('Cross', 'WHEEL', 'Left')]).errors.length); +}); + +test('new utility actions import and round trip without accepting unsupported sources', () => { + const source = 'StickModifier = MOUSE:X2\nShake = MOUSE:Left\nTogglePerformanceOverlay = KEY:F9\nScreenshot = KEY:F8\n'; + const imported = parseIni(source); + assert.deepEqual(imported.errors, []); + assert.equal(serialize(imported.entries), source); + for (const action of ['StickModifier', 'Shake', 'TogglePerformanceOverlay', 'Screenshot']) { + assert.ok(validate([binding(action, 'WHEEL', 'Up')]).errors.length); + } + for (const action of ['TogglePerformanceOverlay', 'Screenshot']) { + assert.deepEqual(parseIni(`${action} = MOUSE:Left`).errors.map(error => error.line), [1]); + } +}); + +test('updated built-in inputs warn until their owning action is overridden', () => { + for (const [action, type, value] of [['L2', 'KEY', 'R'], ['L2', 'MOUSE', 'X1'], ['ToggleMouse', 'KEY', 'F7'], ['Shake', 'KEY', 'X'], ['Screenshot', 'KEY', 'F12'], ['TogglePerformanceOverlay', 'KEY', 'F10']]) { + const input = binding('Cross', type, value); + assert.ok(validate([input]).warnings.some(warning => warning.message.includes(`${action} (built-in)`))); + assert.equal(validate([input, binding(action, 'KEY', 'J')]).warnings.length, 0); + } +}); + +test('conflicts warn without blocking, including omitted built-in actions', () => { + const input = [binding('Cross'), binding('Square')]; + const result = validate(input); + assert.equal(result.errors.length, 0); + assert.ok(result.warnings.some(warning => warning.message.includes('Cross and Square and RightStickLeft (built-in)'))); + assert.doesNotThrow(() => serialize(input)); + const overridden = validate([binding('Cross'), binding('RightStickLeft', 'KEY', 'J')]); + assert.equal(overridden.warnings.length, 0); +}); + +test('serialization is deterministic in action order and preserves alternative order', () => { + const input = [binding('ToggleMouse', 'MOUSE', 'X1'), binding('Cross', 'KEY', 'Space'), binding('Cross')]; + assert.equal(serialize(input), 'Cross = KEY:Space\nCross = KEY:F\nToggleMouse = MOUSE:X1\n'); + assert.deepEqual(input, [binding('ToggleMouse', 'MOUSE', 'X1'), binding('Cross', 'KEY', 'Space'), binding('Cross')]); +}); + +test('import supports BOM, CRLF, inline comments, extra equals and colons in key names', () => { + const result = parseIni('\uFEFF# config\r\n cross = key : space ; note\r\nCross=KEY:Space\r\nR2=mouse:right\r\nL1=KEY:=\r\nL2=KEY:Keypad :\r\n'); + assert.deepEqual(result.errors, []); + assert.equal(result.warnings.length, 1); + assert.deepEqual(result.entries, [binding('Cross', 'KEY', 'Space'), binding('R2', 'MOUSE', 'Right'), binding('L1', 'KEY', '='), binding('L2', 'KEY', 'Keypad :')]); +}); + +test('import rejects sections, unsupported extensions and malformed lines with line numbers', () => { + const result = parseIni('[Bindings]\nCross=NONE\nCross=KEY:4\nToggleFullscreen=MOUSE:Left\nLeftStickUp=WHEEL:Up\nCross=KEY:;\nCross=KEY:KeyF\nCross=KEY:\n'); + assert.deepEqual(result.errors.map(error => error.line), [1, 2, 4, 5, 6, 7, 8]); + assert.deepEqual(result.entries, [binding('Cross', 'KEY', '4')]); + assert.throws(() => parseIni(null), TypeError); +}); + +test('round trip is stable for every safe key and supported mouse/wheel source', () => { + const input = [...exportableKeys.map(value => binding('Cross', 'KEY', value)), ...mouseValues.map(value => binding('Square', 'MOUSE', value)), binding('R2', 'WHEEL', 'Down')]; + const text = serialize(input); + const imported = parseIni(text); + assert.equal(imported.errors.length, 0); + assert.equal(serialize(imported.entries), text); +}); + +test('capture maps physical positions and distinguishes modifiers and keypad', () => { + const cases = {KeyA: 'A', Digit1: '1', Enter: 'Return', NumpadEnter: 'Keypad Enter', Numpad1: 'Keypad 1', ControlLeft: 'Left Ctrl', AltRight: 'Right Alt', ArrowUp: 'Up', F24: 'F24', Equal: '=', Backslash: '\\'}; + for (const [code, value] of Object.entries(cases)) assert.deepEqual(captureKey({code, key: 'ignored'}), {value}); + assert.equal(captureKey({code: 'KeyQ', key: 'a'}).value, 'Q'); +}); + +test('capture rejects missing identifiers, composition and unsafe punctuation', () => { + for (const event of [{code: 'Unidentified'}, {key: 'a'}, {code: 'IntlBackslash'}, {code: 'Semicolon'}, {code: 'KeyA', isComposing: true}, {code: 'Quote', key: 'Dead'}, {code: 'KeyA', key: 'Process'}]) { + assert.ok(captureKey(event).error); + } +}); diff --git a/tools/input-mapper/validation.js b/tools/input-mapper/validation.js new file mode 100644 index 0000000000..ce4c7a8bb9 --- /dev/null +++ b/tools/input-mapper/validation.js @@ -0,0 +1,93 @@ +import {actions, canonicalValue, defaults, mouseValues, typesForAction, wheelValues} from './mapping.js'; +import {keyNames} from './keys.js'; + +export function validate(entries) { + const errors = []; + const warnings = []; + const bindings = []; + const seen = new Set(); + if (!Array.isArray(entries)) return {errors: [{message: 'Configuration must be a list of bindings.'}], warnings, bindings}; + entries.forEach((entry, index) => { + const action = canonicalValue(entry?.action, actions); + const type = canonicalValue(entry?.type, ['KEY', 'MOUSE', 'WHEEL']); + let message; + if (!action) message = 'Unknown or empty action.'; + else if (!type) message = 'Input type must be KEY, MOUSE or WHEEL.'; + else if (!typesForAction(action).includes(type)) message = `${action} cannot use ${type}.`; + const choices = type === 'KEY' ? keyNames : type === 'MOUSE' ? mouseValues : wheelValues; + const value = canonicalValue(entry?.value, choices); + if (!message && !value) message = `Unknown or empty ${type} value.`; + if (!message && /[#;\r\n]/u.test(value)) message = 'This SDL key name cannot be represented in the current INI comment syntax.'; + if (message) { + errors.push({index, message: `${entry?.action || 'Binding'}: ${message}`}); + return; + } + const binding = {action, type, value}; + const identity = `${action}:${type}:${value}`; + if (seen.has(identity)) warnings.push({index, message: `${action}: duplicate ${type}:${value} will be omitted.`}); + else bindings.push(binding); + seen.add(identity); + }); + const configured = new Set(bindings.map(binding => binding.action)); + const sources = new Map(); + const effective = bindings.map(binding => ({action: binding.action, source: `${binding.type}:${binding.value}`, builtin: false})); + for (const action of actions) { + if (!configured.has(action)) { + for (const source of defaults[action]) effective.push({action, source, builtin: true}); + } + } + for (const binding of effective) { + const owners = sources.get(binding.source) || []; + owners.push(binding); + sources.set(binding.source, owners); + } + for (const [source, owners] of sources) { + if (owners.length > 1 && owners.some(owner => !owner.builtin)) { + warnings.push({message: `${source} controls ${owners.map(owner => `${owner.action}${owner.builtin ? ' (built-in)' : ''}`).join(' and ')}.`}); + } + } + return {errors, warnings, bindings}; +} + +export function serialize(entries) { + const result = validate(entries); + if (result.errors.length) throw new Error(result.errors.map(error => error.message).join('\n')); + const lines = []; + for (const action of actions) { + for (const binding of result.bindings.filter(binding => binding.action === action)) { + lines.push(`${action} = ${binding.type}:${binding.value}`); + } + } + return lines.length ? `${lines.join('\n')}\n` : ''; +} + +export function parseIni(text) { + if (typeof text !== 'string') throw new TypeError('INI content must be text.'); + const entries = []; + const errors = []; + const warnings = []; + const identities = new Set(); + text.replace(/^\uFEFF/u, '').split('\n').forEach((line, index) => { + const content = line.split(/[#;]/u, 1)[0].trim(); + if (!content) return; + const separator = content.indexOf('='); + const source = content.slice(separator + 1); + const colon = source.indexOf(':'); + if (separator === -1 || colon === -1) { + errors.push({line: index + 1, message: 'Expected Action = TYPE:VALUE.'}); + return; + } + const entry = {action: content.slice(0, separator), type: source.slice(0, colon), value: source.slice(colon + 1)}; + const result = validate([entry]); + if (result.errors.length) { + errors.push(...result.errors.map(error => ({line: index + 1, message: error.message}))); + return; + } + const binding = result.bindings[0]; + const identity = JSON.stringify(binding); + if (identities.has(identity)) warnings.push({line: index + 1, message: 'Duplicate binding ignored.'}); + else entries.push(binding); + identities.add(identity); + }); + return {entries, errors, warnings}; +} diff --git a/tools/nid_names.py b/tools/nid_names.py index a7d1841005..9c32236fe8 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -51,7 +51,7 @@ def load_db(path): if not path.exists(): path.parent.mkdir(parents=True, exist_ok=True) print("downloading NID database to %s ..." % path, file=sys.stderr) - with tempfile.NamedTemporaryFile(dir=path.parent, prefix=path.name + ".", suffix=".tmp", delete=False) as download: + with tempfile.NamedTemporaryFile(dir=path.parent, prefix=".nid-db-", suffix=".tmp", delete=False) as download: temporary = Path(download.name) try: urllib.request.urlretrieve(DB_URL, str(temporary)) @@ -225,7 +225,7 @@ def main(argv=None): db_path = Path(args.db) if args.db else DEFAULT_CACHE try: - db = load_db(db_path) + db = {} if requested_nids and not args.nid else load_db(db_path) if requested_nids: unknowns = {nid: "" for nid in (args.nid or [])} else: diff --git a/tools/package_release.py b/tools/package_release.py index c32336f6d8..ec4549d801 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -112,8 +112,12 @@ def collect_docs(source, output): raise RuntimeError(f"Duplicate release asset name: {document.name}") names.add(document.name.casefold()) output.mkdir(parents=True, exist_ok=True) - for document in documents: - shutil.copy2(document, output / document.name) + with tempfile.TemporaryDirectory(prefix=".docs-", dir=output) as temporary: + staging = Path(temporary) + for document in documents: + shutil.copy2(document, staging / document.name) + for document in documents: + (staging / document.name).replace(output / document.name) def package_web(build, output, version, revision): diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index eadcf79576..992a1e4b2e 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -71,10 +71,10 @@ def depends_on(body): body = re.sub(r"", "", body or "", flags=re.S) repo = re.escape(globals().get("REPO", "boykopovar/AnyPS5")) repo_pattern = rf"(?:{repo}|boykopovar/AnyPS5)" - body = re.sub(rf"\[([^\]]*)\]\(https?://github\.com/{repo_pattern}/pull/(\d+)(?:[#/?][^)\s]*)?\)", r"#\2", body, flags=re.IGNORECASE) - body = re.sub(rf"\[([^\]]*)\]\(https?://github\.com/(?!{repo_pattern}/)[^/\s]+/[^/\s]+/pull/\d+[^)\s]*\)", "", body, flags=re.IGNORECASE) + body = re.sub(rf"\[([^\]]*)\]\(https?://(?:www\.)?github\.com/{repo_pattern}/pull/(\d+)(?:[#/?][^)\s]*)?\)", r"#\2", body, flags=re.IGNORECASE) + body = re.sub(rf"\[([^\]]*)\]\(https?://(?:www\.)?github\.com/(?!{repo_pattern}/)[^/\s]+/[^/\s]+/pull/\d+[^)\s]*\)", "", body, flags=re.IGNORECASE) body = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", body) - body = re.sub(rf"https?://github\.com/{repo_pattern}/pull/(\d+)\b(?:/[^\s]*)?", r"#\1", body, flags=re.IGNORECASE) + body = re.sub(rf"https?://(?:www\.)?github\.com/{repo_pattern}/pull/(\d+)\b(?:/[^\s]*)?", r"#\1", body, flags=re.IGNORECASE) match = next((match for match in DEPENDS.finditer(body) if match.group(1).strip()), None) or PLAIN_DEPENDS.search(body) return {int(n) for n in re.findall(r"#(\d+)", match.group(1))} if match else set() diff --git a/tools/prepare_release_notes.py b/tools/prepare_release_notes.py index 0163999059..0b8c6c72db 100644 --- a/tools/prepare_release_notes.py +++ b/tools/prepare_release_notes.py @@ -3,6 +3,7 @@ import json import os import re import stat +import sys from pathlib import Path BODY_LIMIT = 125000 @@ -26,7 +27,7 @@ def prepare_notes(source, body, full): for second in paths[index + 1:]: if first.resolve() == second.resolve() or (first.exists() and second.exists() and first.samefile(second)): raise ValueError("Release note input and outputs must be distinct files") - notes = json.loads(source.read_text(encoding="utf-8"))["body"] + notes = json.loads(source.read_text(encoding="utf-8-sig"))["body"] if not isinstance(notes, str): raise ValueError("Generated release notes body must be a string") for destination in (body, full): @@ -40,13 +41,21 @@ def prepare_notes(source, body, full): body.write_bytes(release_body(notes).encode("utf-8")) -def main(): +def main(argv=None): + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace") + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="Prepare size-safe GitHub release notes") parser.add_argument("--input", type=Path, required=True) parser.add_argument("--body", type=Path, required=True) parser.add_argument("--full", type=Path, required=True) - args = parser.parse_args() - prepare_notes(args.input, args.body, args.full) + args = parser.parse_args(argv) + try: + prepare_notes(args.input, args.body, args.full) + except (OSError, ValueError) as error: + parser.error(str(error)) + return 0 if __name__ == "__main__": diff --git a/tools/progress.py b/tools/progress.py index a3c38e1e83..aaef552f7b 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -520,6 +520,10 @@ def validate_snapshot(snapshot): def main(argv=None): + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace") + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace") global PRX, OPCODES previous = PRX, OPCODES parser = argparse.ArgumentParser() @@ -539,7 +543,7 @@ def main(argv=None): snapshots = [] for path in args.compare: try: - snapshots.append(json.loads(path.read_text(encoding="utf-8"))) + snapshots.append(json.loads(path.read_text(encoding="utf-8-sig"))) except (OSError, ValueError) as error: parser.error(f"{path}: {error}") for path, snapshot in zip(args.compare, snapshots): @@ -579,6 +583,4 @@ def main(argv=None): if __name__ == "__main__": - if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8") raise SystemExit(main()) diff --git a/tools/progress_history.py b/tools/progress_history.py index b223ab7c58..e98eaa5df8 100644 --- a/tools/progress_history.py +++ b/tools/progress_history.py @@ -25,8 +25,13 @@ def git(*args): def daily_commits(ref): + try: + log = git("log", "--first-parent", "--format=%H %ct", ref) + except subprocess.CalledProcessError as error: + reason = error.stderr.decode("utf-8", errors="replace").strip() + raise ValueError(f"cannot read the history of {ref}: {reason}") from error seen = {} - for line in git("log", "--first-parent", "--format=%H %ct", ref).decode("utf-8").splitlines(): + for line in log.decode("utf-8").splitlines(): sha, timestamp = line.split() seen.setdefault(datetime.fromtimestamp(int(timestamp), timezone.utc).date().isoformat(), sha) return sorted(seen.items()) diff --git a/tools/run_job_manager_reference.py b/tools/run_job_manager_reference.py index d804f3e573..705d0e1228 100644 --- a/tools/run_job_manager_reference.py +++ b/tools/run_job_manager_reference.py @@ -3,24 +3,27 @@ import hashlib import json import os import shutil +import struct import subprocess import sys import tempfile from pathlib import Path from import_audit import read_host_imports -from unwrap_self import unwrap +from unwrap_self import Refused, unwrap ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "core/relinker/relinker/tests")) from guest_import_fixtures import executable -CASES = ("memory", "validation", "dispatch", "ordering", "nested", "limits", "node_pool", "priority", "shutdown", "worker_policy", "worker_wake", "worker_wait", "worker_count", "worker_spin") +CASES = ("memory", "owned_state", "concurrent_wait", "validation", "dispatch", "ordering", "nested", "limits", "node_pool", "priority", "shutdown", "worker_policy", "worker_wake", "worker_wait", "worker_count", "worker_spin") +PARITY_CASES = tuple(case for case in CASES if case not in ("node_pool", "worker_wait")) def stage_libraries(directory, build, toolchain, dependencies): providers = {path.name.lower(): path for path in (build / "core/libs/libs").glob("*.prx")} - providers.update({path.name.lower(): path for path in toolchain.glob("*.dll")}) + if toolchain is not None: + providers.update({path.name.lower(): path for path in toolchain.glob("*.dll")}) pending = list(dependencies) staged = set() while pending: @@ -44,7 +47,8 @@ def run(args, directory): if not helper.is_file(): raise RuntimeError("Build the job_manager_reference target first") shutil.copy2(helper, libraries / "jobref.prx") - subprocess.run([str(args.build / "core/libs/nid_patcher.exe"), "jobref", "--preserve-export-list", + suffix = ".exe" if sys.platform == "win32" else "" + subprocess.run([str(args.build / f"core/libs/nid_patcher{suffix}"), "jobref", "--preserve-export-list", str(args.build / "core/libs/libc.exports"), str(libraries / "jobref.prx")], check=True, capture_output=True) module_hash = None if args.module: @@ -58,14 +62,19 @@ def run(args, directory): else: unwrap(args.module, target) source = directory / "input.elf" - source.write_bytes(executable("jobref.prx", symbol="Run_nid_no_patch")) - output = directory / "reference.exe" + entry = executable("jobref.prx", symbol="Run_nid_no_patch") + entry[0x4000:0x400f] = b"\x48\x83\xec\x08\xff\x15" + struct.pack(" 30: parser.error("--timeout must be greater than zero and at most 30 seconds") try: @@ -124,7 +144,7 @@ def main(): directory.mkdir(parents=True, exist_ok=False) print(f"Artifacts: {directory}", flush=True) return run(args, directory) - except (OSError, RuntimeError, subprocess.SubprocessError) as error: + except (OSError, Refused, RuntimeError, subprocess.SubprocessError) as error: print(str(error), file=sys.stderr) if isinstance(error, subprocess.CalledProcessError) and error.stderr: print(error.stderr.decode(errors="replace"), file=sys.stderr) diff --git a/tools/select_tests.py b/tools/select_tests.py index 02b15cfbbe..1fa013a3a9 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -169,14 +169,32 @@ def chunks(names): yield batch +def load_graph(path): + graph = json.loads(Path(path).read_text(encoding="utf-8-sig")) + if not isinstance(graph, dict) or not isinstance(graph.get("tests"), dict) or not isinstance(graph.get("closures"), dict): + raise ValueError(f"{path}: invalid test graph (expected tests and closures objects)") + fields = ("registered", "arguments", "sets", "requires", "depends") + for name, test in graph["tests"].items(): + if (not isinstance(name, str) or not isinstance(test, dict) or "target" not in test + or not (test["target"] is None or isinstance(test["target"], str)) + or any(not isinstance(test.get(field), list) + or any(not isinstance(item, str) for item in test[field]) for field in fields)): + raise ValueError(f"{path}: invalid test graph entry {name!r}") + for name, paths in graph["closures"].items(): + if not isinstance(name, str) or not isinstance(paths, list) or any(not isinstance(path, str) for path in paths): + raise ValueError(f"{path}: invalid test graph closure {name!r}") + return graph + + def dump(build, out): deps = collections.defaultdict(set) current = None - for line in build.run("-t", "deps").splitlines(): - if not line.startswith(" ") and ": #deps" in line: - current = build.key(line.split(": #deps")[0]) + recorded = subprocess.run([build.program, "-C", build.path, "-t", "deps"], capture_output=True, check=True).stdout + for line in recorded.splitlines(): + if not line.startswith(b" ") and b": #deps" in line: + current = build.key(line.split(b": #deps")[0].decode(build.encoding)) elif line.strip() and current: - path = build.repo(line.strip()) + path = build.repo(line.strip().decode("utf-8")) if path: deps[current].add(path) listing, listed = build.tests() @@ -301,7 +319,7 @@ def main(argv=None): return subprocess.run(["ctest", "--test-dir", args.build, *args.arguments, *extra]).returncode if args.command == "explain": try: - graph = json.loads(args.graph.read_text(encoding="utf-8-sig")) + graph = load_graph(args.graph) changed = [line.strip() for line in args.changed.read_text(encoding="utf-8-sig").splitlines() if line.strip()] reasons = {} picked, full = select(graph, changed, reasons) @@ -323,8 +341,11 @@ def main(argv=None): if not os.path.exists(args.graph): print("full run: no test graph") return 0 - graph = json.loads(Path(args.graph).read_text(encoding="utf-8-sig")) - changed = [line.strip() for line in Path(args.changed).read_text(encoding="utf-8-sig").splitlines() if line.strip()] + try: + graph = load_graph(args.graph) + changed = [line.strip() for line in Path(args.changed).read_text(encoding="utf-8-sig").splitlines() if line.strip()] + except (OSError, ValueError) as error: + parser.error(str(error)) picked, full = select(graph, changed) if picked is None: print(f"full run: {', '.join(full[:5])}") diff --git a/tools/stubgen.py b/tools/stubgen.py index 7426cd8aa2..436233cb23 100644 --- a/tools/stubgen.py +++ b/tools/stubgen.py @@ -187,7 +187,7 @@ def write_outputs(outputs): path = Path(filename).resolve() if path.is_dir(): raise IsADirectoryError(str(path)) - with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", newline="\n", dir=path.parent, prefix=path.name + ".", suffix=".tmp", delete=False) as handle: + with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", newline="\n", dir=path.parent, prefix=".stubgen-", suffix=".tmp", delete=False) as handle: temporary = Path(handle.name) staged.append((temporary, path)) handle.write(content) diff --git a/tools/suggest_patches.py b/tools/suggest_patches.py index 51d91d85a1..f927d31dc6 100644 --- a/tools/suggest_patches.py +++ b/tools/suggest_patches.py @@ -38,6 +38,19 @@ def command_line(arguments): return subprocess.list2cmdline(arguments) if os.name == "nt" else shlex.join(arguments) +def conversion_options(args, patch): + options = ["--patches", str(patch)] + if args.skip_sce_module: + options.append("--skip-sce-module") + if args.sce_module_path: + options.extend(("--sce-module-path", str(args.sce_module_path))) + for module_dir in args.module_dir: + options.extend(("--module-dir", module_dir)) + for executable in args.executable: + options.extend(("--executable", executable)) + return command_line(options) + + def load_catalog(directory, title, version): data = json.loads(confined(directory, "index.json").read_text(encoding="utf-8")) if not isinstance(data, dict) or set(data) != {"format", "entries"} or type(data["format"]) is not int or data["format"] != 1 or not isinstance(data["entries"], list): @@ -69,6 +82,9 @@ def validate_entry(directory, entry): 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="Recommend a local patch only after metadata, hashes and relinker verification match") parser.add_argument("--catalog", required=True, type=Path, help="directory containing index.json and patch files") parser.add_argument("--param-json", required=True, type=Path, help="title metadata file") @@ -79,7 +95,9 @@ def main(argv=None): parser.add_argument("--executable", action="append", default=[], help="additional title executable relative to the module parent; repeatable") parser.add_argument("--skip-sce-module", action="store_true", help="skip bundled modules during verification") parser.add_argument("--timeout-seconds", type=int, default=120, help="maximum seconds per relinker verification (default: 120)") + parser.add_argument("--list-verified", action="store_true", help="list every verified catalog entry without choosing one") args = parser.parse_args(argv) + prefix = "No verified catalog entries" if args.list_verified else "No recommendation" try: metadata = json.loads(args.param_json.read_text(encoding="utf-8")) if not isinstance(metadata, dict): @@ -102,7 +120,7 @@ def main(argv=None): matches = [] entries = load_catalog(args.catalog.resolve(), title, version) if not entries: - print(f"No recommendation: no catalog entry for {title} {version}.") + print(f"{prefix}: no catalog entry for {title} {version}.") return 1 relinker_errors = False for number, entry in entries: @@ -135,7 +153,8 @@ def main(argv=None): command.extend(("--executable", executable)) command.append(str(args.input)) try: - result = subprocess.run(command, capture_output=True, text=True, timeout=args.timeout_seconds) + result = subprocess.run(command, capture_output=True, text=True, encoding="utf-8", errors="replace", + timeout=args.timeout_seconds) except subprocess.TimeoutExpired: print(f"{label}: rejected: relinker verification exceeded {args.timeout_seconds} seconds") continue @@ -157,29 +176,29 @@ def main(argv=None): print(f"{label}: rejected: relinker did not confirm every patch") continue matches.append((number, entry, patch, targets)) + if args.list_verified and matches: + print(f"Verified catalog entries: {len(matches)}") + print("Choose one patch file; listed entries are not combined.") + for number, entry, patch, targets in matches: + print(f"Catalog entry {number}: {patch}") + for name, digest in targets.items(): + print(f" {name} SHA-256 {digest}") + print(f"Use these options when converting:\n {conversion_options(args, patch)}") + return 0 if len(matches) == 1: number, entry, patch, targets = matches[0] print(f"Recommended: {patch} (catalog entry {number})") for name, digest in targets.items(): print(f" {name} SHA-256 {digest}") - conversion = ["--patches", str(patch)] - if args.skip_sce_module: - conversion.append("--skip-sce-module") - if args.sce_module_path: - conversion.extend(("--sce-module-path", str(args.sce_module_path))) - for module_dir in args.module_dir: - conversion.extend(("--module-dir", module_dir)) - for executable in args.executable: - conversion.extend(("--executable", executable)) - print(f"Use these options when converting:\n {command_line(conversion)}") + print(f"Use these options when converting:\n {conversion_options(args, patch)}") return 0 if len(matches) > 1: - print("No recommendation: multiple verified catalog entries match.") + print("No recommendation: multiple verified catalog entries match; use --list-verified to see them.") return 1 if relinker_errors: - print("No recommendation: relinker errors occurred; see catalog entries above.", file=sys.stderr) + print(f"{prefix}: relinker errors occurred; see catalog entries above.", file=sys.stderr) return 2 - print("No recommendation: no catalog entry passed all checks.") + print(f"{prefix}: no catalog entry passed all checks.") return 1 except (OSError, ValueError, json.JSONDecodeError, tomllib.TOMLDecodeError) as error: print(f"Patch advisor: {error}", file=sys.stderr) diff --git a/tools/symbolize_samples.py b/tools/symbolize_samples.py new file mode 100644 index 0000000000..1536075235 --- /dev/null +++ b/tools/symbolize_samples.py @@ -0,0 +1,300 @@ +import argparse +import bisect +import collections +import shutil +import struct +import subprocess +import sys +from pathlib import Path + +DESCRIPTION = """Symbolize the output of the Windows samplers (APS5_SAMPLE_WORKER, APS5_SAMPLE_COMMITTER and +APS5_SAMPLE_THREADS) against the modules that ran. Each sampled frame is written as module+0xRVA; this reads the +COFF symbol table of each module found in --libs (the MinGW-built .prx and .dll files keep one), or its export +table when it has none, and names every frame module!function. + +The '# module name size timestamp' header lines the samplers write are compared with the SizeOfImage and +TimeDateStamp of the file that is found; a module whose file does not match is left unsymbolized, since its +addresses would name the wrong functions. + +The sampler rewrites its files every 20 s with totals since start-up. --since takes an earlier copy of the same +file and subtracts it, which keeps only the samples taken between the two copies (a steady-state window). + +A module with no such record is also left unsymbolized, since nothing shows which build it was; --unverified +symbolizes it anyway against the file found. + +Input: a collapsed-stack file (.collapsed.txt, 'frame;...;frame count' lines) or an address file +('module offset self inclusive' lines, under 'thread samples ' headings for APS5_SAMPLE_THREADS; --thread +keeps one thread). Output: the hottest functions by self and by inclusive samples, and with --out a symbolized +collapsed-stack file for speedscope.app or flamegraph.pl. For an address file, a function's inclusive count is the +sum of its addresses' counts, which overcounts the samples in which it is on the stack more than once.""" + +IMAGE_SCN_CNT_CODE = 0x20 +IMAGE_SYM_CLASS_EXTERNAL = 2 +IMAGE_SYM_CLASS_STATIC = 3 + + +class PeModule: + def __init__(self, path): + data = Path(path).read_bytes() + if data[:2] != b"MZ": + raise ValueError(f"{path}: not a PE file") + pe = struct.unpack_from("> 4) != 2 and value == 0: + continue + if name[:4] == b"\0\0\0\0": + at = strings + struct.unpack_from(" samples '") + samples.stacks[frames] += int(count) + return samples + + +def subtract(later, earlier): + result = collections.Counter(later) + result.subtract(earlier) + return collections.Counter({key: count for key, count in result.items() if count > 0}) + + +def window(later, earlier): + result = Samples() + result.headers = dict(later.headers) + for name in ("stacks", "self_counts", "inclusive", "total"): + setattr(result, name, subtract(getattr(later, name), getattr(earlier, name))) + return result + + +def summarize(symbolizer, samples, top): + self_counts = collections.Counter() + inclusive = collections.Counter() + total = 0 + if samples.stacks: + for frames, count in samples.stacks.items(): + names = [symbolizer.frame(frame) for frame in frames.split(";")] + total += count + self_counts[names[-1]] += count + for name in set(names): + inclusive[name] += count + else: + for (module, offset), count in samples.self_counts.items(): + self_counts[symbolizer.frame(f"{module}+{offset}")] += count + for (module, offset), count in samples.inclusive.items(): + inclusive[symbolizer.frame(f"{module}+{offset}")] += count + total = samples.total["threads"] or samples.total["samples"] or sum(samples.self_counts.values()) + lines = [f"{total} samples"] + for title, counter in (("self", self_counts), ("inclusive", inclusive)): + if not counter: + continue + lines.append(f"hottest by {title}:") + for name, count in counter.most_common(top): + lines.append(f" {count:8d} {100.0 * count / total if total else 0.0:5.1f}% {name}") + return lines + + +def main(argv=None): + parser = argparse.ArgumentParser(description=DESCRIPTION, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("samples", help="collapsed-stack file or address file written by a Windows sampler") + parser.add_argument("--libs", action="append", required=True, help="directory holding the modules that ran (repeatable)") + parser.add_argument("--since", help="an earlier copy of the same file, subtracted to keep the samples taken after it") + parser.add_argument("--out", help="write the symbolized collapsed stacks to this file") + parser.add_argument("--top", type=int, default=30, help="functions listed per table (default 30)") + parser.add_argument("--demangle", action="store_true", help="demangle C++ names with c++filt when it is on PATH") + parser.add_argument("--thread", help="keep only this thread of an APS5_SAMPLE_THREADS file") + parser.add_argument("--unverified", action="store_true", help="also symbolize modules the samples have no '# module' record for") + args = parser.parse_args(argv) + samples = load(args.samples, args.thread) + if args.since: + samples = window(samples, load(args.since, args.thread)) + symbolizer = Symbolizer(args.libs, samples.headers, args.demangle, warn=lambda text: print("warning: " + text, file=sys.stderr), unverified=args.unverified) + for line in summarize(symbolizer, samples, args.top): + print(line) + if args.out: + if not samples.stacks: + parser.error("--out needs a collapsed-stack file") + with open(args.out, "w", encoding="utf-8") as output: + for frames, count in samples.stacks.items(): + output.write(";".join(symbolizer.frame(frame) for frame in frames.split(";")) + f" {count}\n") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/test_web.py b/tools/test_web.py index 9da700335f..03fe29a0c7 100644 --- a/tools/test_web.py +++ b/tools/test_web.py @@ -26,7 +26,9 @@ def main(argv=None): if not archive.is_file(): parser.error(f"archive module does not exist: {archive}") try: - subprocess.run(["node", "--test", str(ROOT / "demo/web/tests/output-directory.test.mjs")], check=True) + subprocess.run(["node", "--test", *(str(ROOT / "demo/web/tests" / name) for name in ( + "app.test.mjs", "batch.test.mjs", "output-directory.test.mjs" + ))], check=True) with tempfile.TemporaryDirectory(prefix="anyps5-web-") as directory: fixture = Path(directory) (fixture / "main.elf").write_bytes(main_fixture()) diff --git a/tools/tests/test_audit_dump.py b/tools/tests/test_audit_dump.py new file mode 100644 index 0000000000..1578239720 --- /dev/null +++ b/tools/tests/test_audit_dump.py @@ -0,0 +1,392 @@ +import contextlib +import io +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path, PureWindowsPath +from unittest import mock + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import audit_dump +import unwrap_self + +SELF = b"\x4f\x15\x3d\x1d" + b"\0" * 60 +ELF = b"\x7fELF" + b"\0" * 60 + + +def fake_unwrap(source, destination, verbose=False): + data = Path(source).read_bytes() + if not data.startswith(SELF[:4]): + raise unwrap_self.Refused("not a SELF container") + Path(destination).parent.mkdir(parents=True, exist_ok=True) + Path(destination).write_bytes(ELF + Path(source).name.encode()) + return len(data) + + +class AuditDumpTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.root = Path(self.directory.name) + self.dump = self.root / "app0" + self.dump.mkdir() + (self.dump / "eboot.bin").write_bytes(SELF) + self.relinker = self.root / "relinker.exe" + self.relinker.write_bytes(b"") + patcher = mock.patch.object(unwrap_self, "unwrap", side_effect=fake_unwrap) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + self.directory.cleanup() + + def module(self, relative, data=SELF): + path = self.dump / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(data) + + def test_every_module_location_is_staged_and_the_extra_ones_become_module_dirs(self): + self.module("sce_module/libc.prx") + self.module("Media/Modules/Il2CppUserAssemblies.prx") + self.module("Media/Plugins/PSNCore.prx", ELF) + self.module("libfmod.prx") + self.module("Media/Plugins/readme.txt", b"text") + (self.dump / "sce_modules").mkdir() + source = self.root / "work" / "source" + directories, extra = audit_dump.prepare(self.dump, source, log=lambda text: None) + self.assertTrue((source / "eboot.elf").read_bytes().startswith(ELF)) + self.assertTrue((source / "sce_module" / "libc.prx").read_bytes().endswith(b"libc.prx")) + self.assertEqual((source / "Media/Plugins/PSNCore.prx").read_bytes(), ELF) + self.assertTrue((source / "libfmod.prx").is_file()) + self.assertFalse((source / "Media/Plugins/readme.txt").exists()) + self.assertEqual(extra, ["Media/Modules", "Media/Plugins", "."]) + self.assertEqual(directories, [source / "sce_module", source / "Media/Modules", source / "Media/Plugins", source]) + + def test_a_module_that_is_neither_a_self_nor_an_elf_is_refused(self): + self.module("sce_module/broken.prx", b"MZ" + b"\0" * 62) + with self.assertRaises(unwrap_self.Refused): + audit_dump.prepare(self.dump, self.root / "source", log=lambda text: None) + + def test_the_relinker_gets_the_registry_windows_and_module_dir_options(self): + with mock.patch.object(subprocess, "run", return_value=subprocess.CompletedProcess([], 0, "", "")) as run: + audit_dump.convert(self.relinker, self.root / "source", self.root / "out" / "app.exe", ["Media/Modules", "."], True) + command = run.call_args.args[0] + self.assertEqual(command[:3], [str(self.relinker), "--registry", "--windows"]) + self.assertEqual(command[3:7], ["--module-dir", "Media/Modules", "--module-dir", "."]) + self.assertEqual(command[7:], [str(self.root / "source" / "eboot.elf"), str(self.root / "out" / "app.exe")]) + + def run_main(self, *arguments): + errors = io.StringIO() + printed = io.StringIO() + with contextlib.redirect_stderr(errors), contextlib.redirect_stdout(printed): + code = audit_dump.main([str(argument) for argument in arguments]) + return code, errors.getvalue() + + def test_the_audit_gets_every_registry_library_and_module_directory(self): + self.module("sce_module/libc.prx") + work = self.root / "work" + + def relink(command, capture_output, text): + output = Path(command[-1]).parent + (output / "app.registry.json").write_text("[]") + (output / "app.libc.prx.guest.prx.registry.json").write_text("[]") + (output / "app.libc.prx.guest.prx.needed.json").write_text("{}") + return subprocess.CompletedProcess(command, 0, "", "") + + with mock.patch.object(subprocess, "run", side_effect=relink), mock.patch.object(audit_dump.import_audit, "main", return_value=1) as audit: + code, _ = self.run_main(self.dump, "--relinker", self.relinker, "--libs", "libs-a", "--libs", "libs-b", "--work", work, "--json", "report.json") + self.assertEqual(code, 1) + argv = audit.call_args.args[0] + output = work / "output" + self.assertEqual(argv[:2], [str(output / "app.libc.prx.guest.prx.registry.json"), str(output / "app.registry.json")]) + self.assertEqual(argv[2:], ["--libs", "libs-a", "--libs", "libs-b", "--modules", str(work / "source" / "sce_module"), "--json", "report.json"]) + + def test_a_reused_work_directory_does_not_audit_registries_left_by_an_earlier_run(self): + work = self.root / "work" + stale = work / "output" / "app.old.prx.guest.prx.registry.json" + stale.parent.mkdir(parents=True) + stale.write_text("[]") + (work / "source" / "sce_module").mkdir(parents=True) + (work / "source" / "sce_module" / "gone.prx").write_bytes(ELF) + + def relink(command, capture_output, text): + (Path(command[-1]).parent / "app.registry.json").write_text("[]") + return subprocess.CompletedProcess(command, 0, "", "") + + with mock.patch.object(subprocess, "run", side_effect=relink), mock.patch.object(audit_dump.import_audit, "main", return_value=0) as audit: + code, _ = self.run_main(self.dump, "--relinker", self.relinker, "--libs", "libs", "--work", work) + self.assertEqual(code, 0) + self.assertEqual(audit.call_args.args[0][:2], [str(work / "output" / "app.registry.json"), "--libs"]) + self.assertFalse((work / "source" / "sce_module" / "gone.prx").exists()) + + def test_an_undeletable_stale_registry_stops_before_conversion(self): + work = self.root / "work" + stale = work / "output" / "locked" / "stale.registry.json" + stale.parent.mkdir(parents=True) + stale.write_text("[]") + real_rmtree = audit_dump.shutil.rmtree + + def rmtree(path, ignore_errors=False, **kwargs): + if Path(path) == work / "output": + if ignore_errors: + return None + raise PermissionError(13, "Permission denied", str(stale)) + return real_rmtree(path, ignore_errors=ignore_errors, **kwargs) + + with mock.patch.object(audit_dump.shutil, "rmtree", side_effect=rmtree), mock.patch.object(audit_dump, "convert") as convert, mock.patch.object(audit_dump.import_audit, "main", return_value=0) as audit: + code, errors = self.run_main(self.dump, "--relinker", self.relinker, "--libs", "libs", "--work", work) + self.assertEqual(code, 2) + self.assertIn("cannot empty", errors) + self.assertIn(str(work / "output"), errors) + convert.assert_not_called() + audit.assert_not_called() + self.assertTrue(stale.exists()) + + @unittest.skipIf(os.name == "nt" or (hasattr(os, "geteuid") and os.geteuid() == 0), "needs POSIX permissions and a non-root user") + def test_a_non_writable_stale_directory_stops_before_conversion(self): + work = self.root / "work" + locked = work / "output" / "locked" + locked.mkdir(parents=True) + (locked / "stale.registry.json").write_text("[]") + locked.chmod(0o500) + try: + with mock.patch.object(audit_dump, "convert") as convert, mock.patch.object(audit_dump.import_audit, "main", return_value=0) as audit: + code, errors = self.run_main(self.dump, "--relinker", self.relinker, "--libs", "libs", "--work", work) + self.assertEqual(code, 2) + self.assertIn("cannot empty", errors) + convert.assert_not_called() + audit.assert_not_called() + finally: + locked.chmod(0o700) + + def test_a_generated_directory_that_survives_removal_stops_before_conversion(self): + work = self.root / "work" + (work / "source").mkdir(parents=True) + with mock.patch.object(audit_dump.shutil, "rmtree"), mock.patch.object(audit_dump, "convert") as convert, mock.patch.object(audit_dump.import_audit, "main", return_value=0) as audit: + code, errors = self.run_main(self.dump, "--relinker", self.relinker, "--libs", "libs", "--work", work) + self.assertEqual(code, 2) + self.assertIn("still exists", errors) + convert.assert_not_called() + audit.assert_not_called() + + def assert_overlap_rejected(self, dump, work): + marker = dump / "original-marker" + marker.write_bytes(b"preserve original") + original = (dump / "eboot.bin").read_bytes() + with mock.patch.object(audit_dump, "prepare", return_value=([], [])) as prepare, mock.patch.object(audit_dump, "convert", return_value=subprocess.CompletedProcess([], 0)) as convert, mock.patch.object(audit_dump.import_audit, "main", return_value=0): + with self.assertRaises(SystemExit) as error: + self.run_main(dump, "--relinker", self.relinker, "--libs", "libs", "--work", work) + self.assertEqual(error.exception.code, 2) + prepare.assert_not_called() + convert.assert_not_called() + self.assertEqual((dump / "eboot.bin").read_bytes(), original) + self.assertEqual(marker.read_bytes(), b"preserve original") + + def test_overlapping_work_paths_are_rejected_before_input_is_removed(self): + for name, relative in (("source", "source"), ("output", "output"), ("nested-source", "source/nested"), ("nested-output", "output/nested"), ("work-is-dump", "."), ("work-in-dump", "ancestor")): + with self.subTest(name=name): + root = self.root / name + dump = root / relative + dump.mkdir(parents=True) + (dump / "eboot.bin").write_bytes(SELF) + work = dump / "work" if name == "work-in-dump" else root + self.assert_overlap_rejected(dump, work) + + def assert_report_rejected(self, report): + work = self.root / "report-work" + marker = work / "source" / "preserve" + marker.parent.mkdir(parents=True, exist_ok=True) + marker.write_bytes(b"previous work") + original = (self.dump / "eboot.bin").read_bytes() + with mock.patch.object(audit_dump, "prepare", return_value=([], [])) as prepare, mock.patch.object(audit_dump, "convert", return_value=subprocess.CompletedProcess([], 0)) as convert, mock.patch.object(audit_dump.import_audit, "main", return_value=0) as audit: + with self.assertRaises(SystemExit) as error: + self.run_main(self.dump, "--relinker", self.relinker, "--libs", "libs", "--work", work, "--json", report) + self.assertEqual(error.exception.code, 2) + prepare.assert_not_called() + convert.assert_not_called() + audit.assert_not_called() + self.assertEqual((self.dump / "eboot.bin").read_bytes(), original) + self.assertEqual(marker.read_bytes(), b"previous work") + + def test_json_outputs_inside_the_dump_are_rejected_before_staging(self): + for relative in (".", "eboot.bin", "report.json", "new/directory/report.json"): + with self.subTest(relative=relative): + self.assert_report_rejected(self.dump / relative) + + def test_json_output_symlink_aliases_of_the_dump_are_rejected(self): + for name, target, directory in (("file", self.dump / "eboot.bin", False), ("directory", self.dump, True), ("missing", self.dump / "not-created.json", False)): + with self.subTest(name=name): + link = self.root / ("report-link-" + name) + try: + os.symlink(target, link, target_is_directory=directory) + except OSError: + self.skipTest("symbolic links unavailable") + self.assert_report_rejected(link / "new.json" if directory else link) + + def test_windows_resolved_path_spellings_protect_missing_reports(self): + for normal, extended in ( + (PureWindowsPath("C:/dump"), PureWindowsPath("//?/C:/dump")), + (PureWindowsPath("//server/share/dump"), PureWindowsPath("//?/UNC/server/share/dump")), + ): + for report_root, dump_root in ((normal, extended), (extended, normal)): + with self.subTest(report_root=report_root, dump_root=dump_root): + dump = mock.Mock() + dump.resolve.return_value = dump_root + dump.absolute.return_value = normal + dump.rglob.return_value = () + dump.parents = () + dump.is_symlink.return_value = False + report = mock.Mock() + report.resolve.return_value = report_root / "not-created.json" + report.absolute.return_value = normal.parent / "outside-report.json" + report.parent.resolve.return_value = normal.parent + report.parents = () + report.is_symlink.return_value = False + report.parent.parents = () + report.parent.is_symlink.return_value = False + report.exists.return_value = False + self.assertTrue(audit_dump.report_overlaps_dump(report, dump)) + report.resolve.return_value = report_root.parent / "outside-report.json" + self.assertFalse(audit_dump.report_overlaps_dump(report, dump)) + + def test_json_report_links_are_checked_when_resolve_leaves_the_alias(self): + resolve = Path.resolve + for name, target, directory in ( + ("file", self.dump / "missing-report.json", False), + ("directory", self.dump / "missing-directory", True), + ): + with self.subTest(name=name): + link = self.root / ("unresolved-report-" + name) + try: + os.symlink(target, link, target_is_directory=directory) + except OSError: + self.skipTest("symbolic links unavailable") + report = link / "report.json" if directory else link + + def unresolved_alias(path, *args, **kwargs): + if path == link or path.is_relative_to(link): + return path.absolute() + return resolve(path, *args, **kwargs) + + with mock.patch.object(Path, "resolve", unresolved_alias): + self.assert_report_rejected(report) + + def test_json_report_link_cycles_are_rejected_before_staging(self): + first = self.root / "report-cycle-first" + second = self.root / "report-cycle-second" + try: + os.symlink(second, first) + os.symlink(first, second) + except OSError: + self.skipTest("symbolic links unavailable") + self.assert_report_rejected(first) + + def test_repeated_parent_alias_in_link_target_is_not_a_cycle(self): + alias = self.root / "parent-alias" + link = self.root / "report-through-parent" + target = self.root / "external-report.json" + try: + os.symlink(self.root, alias, target_is_directory=True) + os.symlink(alias / target.name, link) + except OSError: + self.skipTest("symbolic links unavailable") + self.assertEqual(audit_dump.comparison_path(alias / link.name), target.resolve()) + self.assertFalse(audit_dump.report_overlaps_dump(alias / link.name, self.dump)) + + def test_json_output_hardlink_aliases_of_dump_files_are_rejected(self): + self.module("Media/Plugins/libfmod.prx") + for name in ("eboot.bin", "Media/Plugins/libfmod.prx"): + with self.subTest(name=name): + link = self.root / (Path(name).name + ".report") + try: + os.link(self.dump / name, link) + except OSError: + self.skipTest("hard links unavailable") + self.assert_report_rejected(link) + + def test_json_output_inside_the_dump_cannot_escape_through_a_symlink(self): + report = self.root / "outside-report.json" + report.write_bytes(b"preserve report") + link = self.dump / "report.json" + try: + os.symlink(report, link) + except OSError: + self.skipTest("symbolic links unavailable") + self.assert_report_rejected(link) + self.assertEqual(report.read_bytes(), b"preserve report") + + def test_json_output_cannot_replace_files_in_a_linked_dump_directory(self): + modules = self.root / "shared-modules" + modules.mkdir() + module = modules / "libc.prx" + module.write_bytes(ELF) + try: + os.symlink(modules, self.dump / "sce_module", target_is_directory=True) + except OSError: + self.skipTest("symbolic links unavailable") + for report in (module, modules / "new-report.json"): + with self.subTest(report=report): + self.assert_report_rejected(report) + self.assertEqual(module.read_bytes(), ELF) + self.assertFalse((modules / "new-report.json").exists()) + + def test_a_dump_alias_still_protects_an_outward_report_link(self): + original = self.dump + alias = self.root / "dump-alias" + report = original / "report.json" + outside = self.root / "not-created.json" + try: + os.symlink(original, alias, target_is_directory=True) + os.symlink(outside, report) + except OSError: + self.skipTest("symbolic links unavailable") + self.dump = alias + try: + self.assert_report_rejected(report) + finally: + self.dump = original + self.assertFalse(outside.exists()) + + def test_symlinked_work_paths_are_checked_against_the_resolved_dump(self): + for generated in ("source", "output"): + with self.subTest(generated=generated): + work = self.root / ("linked-" + generated) + work.mkdir() + try: + os.symlink(self.dump, work / generated, target_is_directory=True) + except OSError: + self.skipTest("symbolic links unavailable") + self.assert_overlap_rejected(self.dump, work) + + def test_a_broken_module_link_fails_instead_of_being_skipped(self): + (self.dump / "sce_module").mkdir() + try: + os.symlink(self.dump / "sce_module" / "missing.prx", self.dump / "sce_module" / "libc.prx") + except OSError: + self.skipTest("symbolic links unavailable") + with mock.patch.object(subprocess, "run") as run: + code, errors = self.run_main(self.dump, "--relinker", self.relinker, "--libs", "libs") + self.assertEqual(code, 2) + self.assertIn("libc.prx", errors) + run.assert_not_called() + + def test_a_relinker_failure_returns_2_with_its_message(self): + with mock.patch.object(subprocess, "run", return_value=subprocess.CompletedProcess([], 3, "", "bad input")), mock.patch.object(audit_dump.import_audit, "main") as audit: + code, errors = self.run_main(self.dump, "--relinker", self.relinker, "--libs", "libs") + self.assertEqual(code, 2) + self.assertIn("relinker exited with 3", errors) + self.assertIn("bad input", errors) + audit.assert_not_called() + + def test_a_dump_without_eboot_or_a_missing_relinker_is_an_argument_error(self): + (self.dump / "eboot.bin").unlink() + with self.assertRaises(SystemExit): + self.run_main(self.dump, "--relinker", self.relinker, "--libs", "libs") + (self.dump / "eboot.bin").write_bytes(SELF) + with self.assertRaises(SystemExit): + self.run_main(self.dump, "--relinker", self.root / "missing.exe", "--libs", "libs") + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_build_web.py b/tools/tests/test_build_web.py index 11c38bfdd4..78cdb95d0e 100644 --- a/tools/tests/test_build_web.py +++ b/tools/tests/test_build_web.py @@ -5,12 +5,14 @@ import io import json from pathlib import Path import subprocess +import struct import sys import tempfile import unittest from unittest.mock import patch import warnings import zipfile +import zlib sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import build_web @@ -26,6 +28,60 @@ def library_zip(entries): class LibraryPublicationTests(unittest.TestCase): + def test_unicode_path_rewrite_is_rejected_before_decompression(self): + original = "libs/../libc.prx" + rewritten = "libs/libc.prx" + entry = zipfile.ZipInfo(original) + unicode_path = b"\x01" + struct.pack(" Texels{};", "alignas(65536)"), + ("alignas(16384) static char block[16];", "alignas(16384)"), + ("alignas( 0x10000 ) char block[16];", "alignas( 0x10000 )"), + ("alignas(0X4000u) char block[16];", "alignas(0X4000u)"), + ("alignas(65'536) char block[16];", "alignas(65'536)"), + ("alignas(040000) char block[16];", "alignas(040000)"), + ("alignas(0b100000000000000) char block[16];", "alignas(0b100000000000000)"), + ("alignas(256) char small[4]; alignas(32768) char large[4];", "alignas(32768)"), + ): + with self.subTest(line=line): + self.assertEqual(over_aligned(line), match) + + def test_alignments_within_the_limit(self): + for line in ( + "alignas(8192) char block[16];", + "alignas(0x2000) char block[16];", + "alignas(256) std::array Input{};", + "alignas(4096) char page[4096];", + "alignas(std::max_align_t) char storage[16];", + "alignas(Shader) char storage[sizeof(Shader)];", + "realignas(65536)", + "constexpr std::size_t TexelAlignment = 65536;", + ): + with self.subTest(line=line): + self.assertIsNone(over_aligned(line)) + + def test_named_constants_of_the_file(self): + self.assertEqual(reported("constexpr std::size_t BlockBytes = 65536;\n" + "static constexpr std::uint32_t PageBytes{0x1000u};\n" + "constexpr auto Large = 0x10000;\n" + "constexpr std::size_t Texels = 4u << 20u;\n" + "alignas(BlockBytes) std::array Block{};\n" + "alignas( Large ) char block[16];\n" + "alignas(PageBytes) char page[4096];\n" + "alignas(Texels) char texels[16];\n" + "alignas(inotify_event) char buffer[16384];\n"), [5, 6]) + self.assertEqual(reported("alignas(BlockBytes) char block[16];\n" + "constexpr std::size_t BlockBytes = 65536;\n"), []) + + def test_constants_in_separate_scopes(self): + self.assertEqual(reported(SMALL + LARGE), [7]) + self.assertEqual(reported(LARGE + SMALL), [3]) + + def test_constants_in_nested_scopes(self): + for outer, inner, expected in ((64, 16384, [6]), (16384, 64, [3, 8])): + text = ("void Nested() {\n" + f" constexpr std::size_t Alignment = {outer};\n" + " alignas(Alignment) static char before[4];\n" + " {\n" + f" constexpr std::size_t Alignment = {inner};\n" + " alignas(Alignment) static char inside[4];\n" + " }\n" + " alignas(Alignment) static char after[4];\n" + "}\n") + with self.subTest(outer=outer, inner=inner): + self.assertEqual(reported(text), expected) + + def test_constants_in_reopened_namespaces(self): + large = "constexpr std::size_t Alignment = 16384;" + use = "alignas(Alignment) static char data[4];" + for text, expected in ( + (f"namespace N {{ {large} }}\nnamespace N {{ {use} }}\n", [2]), + (f"namespace N {{ {large} }}\nnamespace M {{ {use} }}\n", []), + (f"namespace {{ {large} }}\nnamespace {{ {use} }}\n", [2]), + (f"namespace A::B {{ {large} }}\nnamespace A {{ namespace B {{ {use} }} }}\n", [2]), + (f"namespace A {{ {large} }}\nnamespace A::B {{ {use} }}\n", [2]), + (f"namespace A {{ inline namespace V1 {{ {large} }} }}\nnamespace A {{ {use} }}\n", [2]), + (f"namespace A {{ inline namespace V1 {{ {large} }} }}\nnamespace B {{ {use} }}\n", []), + (f"namespace N {{ {large} }}\nnamespace N {{ void F() {{ constexpr std::size_t Alignment = 64; {use} }} }}\n", []), + (f'extern "C" {{ {large} }}\n{use}\n', [2]), + ): + with self.subTest(text=text): + self.assertEqual(reported(text), expected) + + +class AlignmentRuleTests(unittest.TestCase): + def findings(self, after): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + + def git(*args): + return subprocess.run(["git", "-c", f"safe.directory={root}", "-c", "user.name=Conventions Test", + "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + cwd=root, capture_output=True, check=True, encoding="utf-8").stdout + + source = root / SOURCE + source.parent.mkdir(parents=True) + source.write_text("alignas(65536) char existing[16];\n", encoding="utf-8") + git("init", "-q") + git("add", ".") + git("commit", "-qm", "test: add fixture") + source.write_text("alignas(65536) char existing[16];\n" + after, encoding="utf-8") + git("commit", "-qam", "test: add over-aligned blocks") + original = Path.cwd() + try: + os.chdir(root) + check = Check("HEAD~1", "HEAD") + check.code() + finally: + os.chdir(original) + return check.findings + + def test_only_added_code_is_reported(self): + self.assertEqual(self.findings("alignas(256) char small[16];\n" + "const char* text = \"alignas(65536)\";\n" + "alignas(0x10000) char added[16];\n" + "constexpr std::size_t BlockBytes = 65536;\n" + "alignas(BlockBytes) std::array Block{};\n"), + [("alignment", SOURCE, 4, "alignas(0x10000)"), ("alignment", SOURCE, 6, "alignas(BlockBytes)")]) + + def test_constants_are_resolved_in_their_scope(self): + self.assertEqual(self.findings("namespace First {\n" + SMALL + LARGE + "}\nnamespace Second {\n" + LARGE + SMALL + "}\n"), + [("alignment", SOURCE, 9, "alignas(Alignment)"), ("alignment", SOURCE, 15, "alignas(Alignment)")]) + + def test_reopened_namespace_keeps_its_constants(self): + self.assertEqual(self.findings("namespace N {\n" + " constexpr std::size_t Alignment = 16384;\n" + "}\n" + "namespace N {\n" + " alignas(Alignment) static char data[4];\n" + "}\n"), + [("alignment", SOURCE, 6, "alignas(Alignment)")]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_check_conventions_ci_gate.py b/tools/tests/test_check_conventions_ci_gate.py index 200d32f39b..ca942cd702 100644 --- a/tools/tests/test_check_conventions_ci_gate.py +++ b/tools/tests/test_check_conventions_ci_gate.py @@ -86,6 +86,47 @@ class CiGateTests(unittest.TestCase): def test_paths_of_another_trigger_do_not_count(self): self.assertUngated(workflow(" push:\n paths:\n - 'core/**'\n pull_request:\n branches: [main]\n", " tests:\n runs-on: ubuntu-latest\n"), "tests") + def test_flow_mapping_paths_passes(self): + self.assertGated(workflow(" pull_request: {paths: ['core/**']}\n", " tests:\n runs-on: ubuntu-latest\n")) + + def test_flow_mapping_paths_ignore_passes(self): + self.assertGated(workflow(" pull_request: {paths-ignore: ['docs/**']}\n", " tests:\n runs-on: ubuntu-latest\n")) + + def test_flow_mapping_without_paths_fails(self): + self.assertUngated(workflow(" pull_request: {branches: [main]}\n", " tests:\n runs-on: ubuntu-latest\n"), "tests") + + def test_flow_mapping_quoted_values_do_not_count(self): + for filters in ( + "{branches-ignore: ['paths:unused']}", + '{branches-ignore: ["paths-ignore:unused"]}', + "{branches-ignore: ['it''s, paths:unused']}", + r'{branches-ignore: ["escaped\", paths:unused"]}', + "{branches-ignore: ['}, paths: [core/**], {']}", + ): + with self.subTest(filters=filters): + self.assertUngated(workflow(f" pull_request: {filters}\n", " tests:\n runs-on: ubuntu-latest\n"), "tests") + self.assertUngated(f"name: Test\non: {{pull_request: {filters}}}\njobs:\n tests:\n runs-on: ubuntu-latest\n", "tests") + + def test_flow_mapping_nested_paths_do_not_count(self): + self.assertUngated(workflow(" pull_request: {branches: [{paths: ['core/**']}]}\n", " tests:\n runs-on: ubuntu-latest\n"), "tests") + self.assertUngated("name: Test\non: {push: {paths: ['core/**']}, pull_request: {branches: [{paths-ignore: ['docs/**']}]}}\njobs:\n tests:\n runs-on: ubuntu-latest\n", "tests") + + def test_flow_mapping_quoted_keys_and_delimiters_pass(self): + for filters in ( + "{branches: ['release,}'], paths: ['core/**']}", + "{'paths': ['core/**']}", + '{"paths-ignore": ["docs/**"]}', + ): + with self.subTest(filters=filters): + self.assertGated(workflow(f" pull_request: {filters}\n", " tests:\n runs-on: ubuntu-latest\n")) + self.assertGated(f"name: Test\non: {{'pull_request': {filters}, push: {{branches: [main]}}}}\njobs:\n tests:\n runs-on: ubuntu-latest\n") + + def test_top_level_flow_mapping_paths_passes(self): + self.assertGated(f"name: Test\n\non: {{pull_request: {{paths: ['core/**']}}}}\njobs:\n{GATE} tests:\n runs-on: ubuntu-latest\n") + + def test_top_level_flow_paths_of_another_trigger_do_not_count(self): + self.assertUngated("name: Test\n\non: {push: {paths: ['core/**']}, pull_request: {branches: [main]}}\njobs:\n" + GATE + " tests:\n runs-on: ubuntu-latest\n", "tests") + def test_inline_trigger_list_fails(self): self.assertUngated("name: Test\non: [push, pull_request]\njobs:\n tests:\n runs-on: ubuntu-latest\n", "tests") diff --git a/tools/tests/test_check_conventions_pr_template.py b/tools/tests/test_check_conventions_pr_template.py index 51412f21dd..35ff674f59 100644 --- a/tools/tests/test_check_conventions_pr_template.py +++ b/tools/tests/test_check_conventions_pr_template.py @@ -71,8 +71,8 @@ class PullRequestTemplateTests(unittest.TestCase): self.assertTrue(items) result, findings = self.run_checker(template) self.assertEqual(result.returncode, 1, result.stdout) - self.assertIn("section 'What' is missing or empty", findings) - self.assertIn("section 'Tested' is missing or empty", findings) + self.assertIn("fill the '### What' section", findings) + self.assertIn("fill the '### Tested' section", findings) for item in items: self.assertIn(f"unchecked: {item.strip()}", findings) self.assertIn("answer AI-assisted with yes or no", findings) @@ -109,9 +109,28 @@ class PullRequestTemplateTests(unittest.TestCase): def test_sections_must_have_content(self): checklist = "- [x] Depends on: none\n- [x] AI-assisted: yes\n" - self.assert_findings(body(checklist, what=""), ["section 'What' is missing or empty"]) - self.assert_findings(body(checklist, tested=" \n"), ["section 'Tested' is missing or empty"]) - self.assert_findings(body(checklist).replace("### What", "#### What"), ["section 'What' is missing or empty"]) + self.assert_findings(body(checklist, what=""), ["fill the '### What' section"]) + self.assert_findings(body(checklist, tested=" \n"), ["fill the '### Tested' section"]) + self.assert_findings(body(checklist).replace("### What", "#### What"), + ["add the '### What' heading of the template; found '#### What'"]) + + def test_a_heading_at_another_level_or_case_is_named(self): + checklist = "- [x] Depends on: none\n- [x] AI-assisted: yes\n" + cases = { + "## What": "add the '### What' heading of the template; found '## What'", + " ### what ": "add the '### What' heading of the template; found '### what'", + "#WHAT": "add the '### What' heading of the template; found '#WHAT'", + } + for heading, finding in cases.items(): + with self.subTest(heading=heading): + self.assert_findings(body(checklist).replace("### What", heading), [finding]) + self.assert_findings(body(checklist).replace("### What", "### Whatever"), ["add the '### What' heading of the template"]) + + def test_a_body_without_template_headings_names_each_heading(self): + text = "What\nFix x.\n\nTested\nNot tested.\n\nChecklist\n- [x] Depends on: none\n- [x] AI-assisted: yes\n" + self.assert_findings(text, ["add the '### What' heading of the template", + "add the '### Tested' heading of the template", + "add the '### Checklist' heading of the template"]) def test_title_must_be_conventional(self): text = body("- [x] Depends on: none\n- [x] AI-assisted: yes\n") diff --git a/tools/tests/test_check_main_sync.py b/tools/tests/test_check_main_sync.py index a82463563e..209c771009 100644 --- a/tools/tests/test_check_main_sync.py +++ b/tools/tests/test_check_main_sync.py @@ -67,6 +67,17 @@ class CheckMainSyncTests(unittest.TestCase): self.assertEqual((data["behind"], data["ahead"], data["conflicts"], data["shared"]), (0, 1, [], [])) self.assertEqual(run_tool(self.repo.path, "--head", "feature", "--strict").returncode, 0) + def test_paths_named_like_revisions_do_not_confuse_the_report(self): + self.branch("feature") + self.repo.write("other.txt", "y\n") + self.repo.commit("feature change") + for name in ("main", "feature", "feature..main", "main..feature"): + self.repo.write(name, "path matching a revision\n") + result = run_tool(self.repo.path, "--head", "feature", "--strict") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("1 ahead and 0 behind main", result.stdout) + self.assertIn("merge: clean", result.stdout) + def test_main_with_explicit_argv(self): self.branch("feature") self.repo.write("other.txt", "y\n") @@ -121,6 +132,51 @@ class CheckMainSyncTests(unittest.TestCase): self.assertIn("1 file(s) changed on both sides", result.stdout) self.assertIn("#77", result.stdout) + def check_squash_merge(self, conflict): + self.branch("feature") + self.repo.write("shared.txt", "one\nmine\nthree\n" if conflict else "zero\none\ntwo\nthree\n") + self.repo.commit("feature edit") + self.repo.git("checkout", "-q", "main") + self.repo.git("checkout", "-q", "-b", "theirs") + self.repo.write("shared.txt", "one\ntheirs\nthree\n" if conflict else "one\ntwo\nthree\nfour\n") + self.repo.commit("their edit") + self.repo.git("checkout", "-q", "main") + self.repo.git("merge", "--squash", "-q", "theirs") + self.repo.commit("fix(test): change the shared file (#4242)") + revision = check_main_sync.git(self.repo.path, "rev-parse", "--short", "main").stdout.strip() + data = check_main_sync.report(self.repo.path, "main", "feature") + self.assertEqual(data["pulls"], {4242: revision}) + self.assertEqual(data["shared"], ["shared.txt"]) + self.assertEqual(data["conflicts"], ["shared.txt"] if conflict else []) + for strict in ((False,) if conflict else (False, True)): + with self.subTest(strict=strict): + result = run_tool(self.repo.path, "--head", "feature", *(["--strict"] if strict else [])) + self.assertEqual(result.returncode, 1 if conflict or strict else 0, result.stderr) + self.assertIn("merge: CONFLICT" if conflict else "merge: clean", result.stdout) + self.assertIn("merged pull requests that touched them: #4242", result.stdout) + self.assertNotIn("Traceback", result.stderr) + + def test_clean_squash_merge_names_the_pull_request(self): + self.check_squash_merge(False) + + def test_conflicting_squash_merge_names_the_pull_request(self): + self.check_squash_merge(True) + + def test_squash_numbers_must_end_the_subject_and_touch_shared_paths(self): + self.branch("feature") + self.repo.write("shared.txt", "zero\none\ntwo\nthree\n") + self.repo.commit("feature edit") + self.repo.git("checkout", "-q", "main") + self.repo.write("shared.txt", "one\ntwo\nthree\nfour\n") + self.repo.commit("fix(test): reference (#13) without merging it") + self.assertEqual(check_main_sync.report(self.repo.path, "main", "feature")["pulls"], {}) + self.repo.write("shared.txt", "one\ntwo\nthree\nfour\nfive\n") + self.repo.commit("fix(test): mention (#13) but merge (#4242)") + revision = check_main_sync.git(self.repo.path, "rev-parse", "--short", "main").stdout.strip() + self.repo.write("other.txt", "unrelated\n") + self.repo.commit("fix(test): an unrelated pull request (#9000)") + self.assertEqual(check_main_sync.report(self.repo.path, "main", "feature")["pulls"], {4242: revision}) + def test_a_subdirectory_reports_the_same_conflicts_and_pull_requests(self): (self.repo.path / "docs").mkdir() self.repo.write("docs/notes.txt", "x\n") @@ -166,6 +222,15 @@ class CheckMainSyncTests(unittest.TestCase): result = run_tool(self.repo.path, "--head", "island") self.assertEqual(result.returncode, 2) self.assertIn("no common ancestor", result.stderr) + result = run_tool(self.repo.path, "--head", "missing") + expected = subprocess.run( + ["git", "-C", str(self.repo.path), "merge-base", "main", "missing"], + capture_output=True, + encoding="utf-8", + ).stderr.strip() + self.assertEqual(result.returncode, 2) + self.assertIn(f"git merge-base failed: {expected}", result.stderr) + self.assertNotIn("no common ancestor", result.stderr) def check_unicode_output(self, conflict): name = "sp ace \u00e9 \u6e2c \U0001f680.txt" diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index 6e3871733b..567a04612c 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -37,6 +37,102 @@ OTHER = [ class CheckSkipsTests(unittest.TestCase): + def test_ctest_status_and_prefixed_device_reasons_are_expected(self): + reports = ( + ('skipped, the device lacks geometryShader', "linux"), + ('f16 native: skipped, the device has no native f16 with RTE rounding and kept denormals', "linux"), + ('video: skipped, no display or Vulkan device: ErrorInitializationFailed', "windows"), + ('skipped, no usable Vulkan device: none', "windows"), + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + for report, job in reports: + with self.subTest(job=job, report=report): + junit.write_text(f'{report}', encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("all expected", result.stdout) + + def test_ctest_status_does_not_allow_missing_or_unexpected_reasons(self): + reports = ( + '', + '', + 'skipped, flaky\nskipped, the device lacks geometryShader', + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + for report in reports: + for job in ("linux", "windows"): + with self.subTest(job=job, report=report): + junit.write_text(f'{report}', encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("error: guest_probe skipped", result.stdout) + + def test_ctest_status_preserves_high_refresh_policy(self): + reason = "skipped, current display does not provide 120 Hz" + reports = ( + (f'{reason}', 0), + (f'{reason}', 0), + (f'{reason}\nunexpected failure detail', 1), + (f'{reason}\nskipped, flaky', 1), + (f'skipped, no display\n{reason}', 1), + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + for report, expected_code in reports: + for job in ("linux", "windows"): + with self.subTest(job=job, report=report): + junit.write_text(f'{report}', encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, expected_code, result.stdout + result.stderr) + + def test_list_reports_device_reason_after_ctest_status(self): + xml = ('' + 'f16 native: skipped, the device has no native f16') + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + junit.write_text(xml, encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(TOOL), str(junit), "--list"], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("guest_probe: skipped, the device has no native f16", result.stdout) + self.assertNotIn("SKIP_RETURN_CODE", result.stdout) + + def test_unexpected_skip_reason_cannot_hide_behind_a_device_limitation(self): + reports = ( + ('skipped, the device lacks geometryShader', "linux"), + ('skipped, no display', "windows"), + ('flakyskipped, no usable Vulkan device', "windows"), + ('skipped, flaky\nskipped, the device lacks geometryShader', "linux"), + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + for report, job in reports: + with self.subTest(job=job, report=report): + junit.write_text(f'{report}', encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("error: guest_probe skipped", result.stdout) + + def test_multiple_allowed_skip_reasons_remain_expected(self): + reports = ( + ('skipped, no display', "linux"), + ('skipped, no usable Vulkan device', "windows"), + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + for report, job in reports: + with self.subTest(job=job, report=report): + junit.write_text(f'{report}', encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + def test_non_junit_xml_fails_in_every_cli_mode(self): reports = ('', 'coverage') with tempfile.TemporaryDirectory() as directory: @@ -249,6 +345,40 @@ class CheckSkipsTests(unittest.TestCase): +class RequiredTestsTests(unittest.TestCase): + def test_required_reports_without_testcases_fail_in_both_jobs(self): + for xml in ('', ''): + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + junit.write_text(xml, encoding="utf-8") + for job in ("linux", "windows"): + for options in ([], ["--list"]): + with self.subTest(xml=xml, job=job, options=options): + result = subprocess.run( + [sys.executable, "-I", str(TOOL), str(junit), "--job", job, "--require-tests", *options], + capture_output=True, text=True, timeout=5) + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("no testcases", result.stderr) + result = subprocess.run( + [sys.executable, "-I", str(TOOL), str(junit), "--job", job, *options], + capture_output=True, text=True, timeout=5) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_executed_tests_without_skips_pass_in_both_jobs(self): + for xml in ('', + ''): + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + junit.write_text(xml, encoding="utf-8") + for job in ("linux", "windows"): + with self.subTest(xml=xml, job=job): + result = subprocess.run( + [sys.executable, "-I", str(TOOL), str(junit), "--job", job, "--require-tests"], + capture_output=True, text=True, timeout=5) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("0 test(s) skipped, all expected", result.stdout) + + class SkipReportTests(unittest.TestCase): def skipped(self, content): with tempfile.TemporaryDirectory() as directory: diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py index 36d6c6a66d..9e0d629857 100644 --- a/tools/tests/test_configure_seed.py +++ b/tools/tests/test_configure_seed.py @@ -238,6 +238,60 @@ class ConfigureSeedRoundTripTests(ConfigureSeedProject): class ChecksParsingTests(unittest.TestCase): + def test_architecture_dependent_sizes_leave_compile_guards_to_cmake(self): + entries = { + "ARCH_SIZE": ("0", "CHECK_TYPE_SIZE: sizeof(architecture_type)"), + "SECOND_SIZE": ("0", "CHECK_TYPE_SIZE: sizeof(other_type)"), + "HAVE_ARCH_SIZE": ("TRUE", "Result of TRY_COMPILE"), + "CUSTOM_RESULT": ("TRUE", "Result of TRY_COMPILE"), + "UNRELATED_COMPILE": ("FALSE", "Result of TRY_COMPILE"), + "CONSTANT_SIZE": ("8", "CHECK_TYPE_SIZE: sizeof(long long)"), + "MISSING_SIZE": ("", "CHECK_TYPE_SIZE: missing_type unknown"), + "HAVE_HEADER": ("1", "Have include stdio.h"), + "SIMPLE_TEST": ("1", "Test SIMPLE_TEST"), + "RUN_RESULT": ("0", "Result of TRY_RUN"), + } + with tempfile.TemporaryDirectory() as directory: + cache = Path(directory) / "CMakeCache.txt" + cache.write_text("".join(f"//{doc}\n{name}:INTERNAL={value}\n" + for name, (value, doc) in entries.items()), encoding="utf-8") + expected = {name: entry for name, entry in entries.items() + if name in {"CONSTANT_SIZE", "MISSING_SIZE", "HAVE_HEADER", "SIMPLE_TEST", "RUN_RESULT"}} + self.assertEqual(configure_seed.checks(cache), expected) + + def test_compile_guards_are_retained_without_architecture_dependent_sizes(self): + entries = { + "CONSTANT_SIZE": ("8", "CHECK_TYPE_SIZE: sizeof(long long)"), + "HAVE_CONSTANT_SIZE": ("TRUE", "Result of TRY_COMPILE"), + "MISSING_SIZE": ("", "CHECK_TYPE_SIZE: missing_type unknown"), + "CUSTOM_RESULT": ("FALSE", "Result of TRY_COMPILE"), + "ZERO_TEST": ("0", "Test ZERO_TEST"), + } + with tempfile.TemporaryDirectory() as directory: + cache = Path(directory) / "CMakeCache.txt" + cache.write_text("".join(f"//{doc}\n{name}:INTERNAL={value}\n" + for name, (value, doc) in entries.items()), encoding="utf-8") + self.assertEqual(configure_seed.checks(cache), entries) + + def test_filters_cannot_seed_architecture_dependent_compile_guards(self): + with tempfile.TemporaryDirectory() as directory: + cache, seed = Path(directory) / "CMakeCache.txt", Path(directory) / "seed.cmake" + cache.write_text("//CHECK_TYPE_SIZE: sizeof(architecture_type)\nARCH_SIZE:INTERNAL=0\n" + "//Result of TRY_COMPILE\nHAVE_ARCH_SIZE:INTERNAL=TRUE\n" + "//Result of TRY_COMPILE\nCUSTOM_RESULT:INTERNAL=TRUE\n" + "//Have include stdio.h\nHAVE_HEADER:INTERNAL=1\n", encoding="utf-8") + for options in ((), ("--include", "*RESULT"), ("--include", "HAVE_*"), + ("--include", "ARCH_SIZE"), ("--exclude", "ARCH_SIZE")): + with self.subTest(options=options): + result = subprocess.run([sys.executable, str(TOOL), str(cache), str(seed), *options], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + text = seed.read_text(encoding="utf-8") + self.assertNotIn("ARCH_SIZE", text) + self.assertNotIn("CUSTOM_RESULT", text) + if not options or options in (("--include", "HAVE_*"), ("--exclude", "ARCH_SIZE")): + self.assertIn("HAVE_HEADER", text) + def test_checks_reads_cache_with_utf8_bom(self): with tempfile.TemporaryDirectory() as directory: cache = Path(directory) / "CMakeCache.txt" diff --git a/tools/tests/test_debt_split.py b/tools/tests/test_debt_split.py index 1f2cf31775..d26cdd035e 100644 --- a/tools/tests/test_debt_split.py +++ b/tools/tests/test_debt_split.py @@ -44,15 +44,14 @@ class DebtSplitTests(unittest.TestCase): self.root = Path(self.directory.name) self.git("init", "-q", "-b", "main") self.write(INDEX, ORIGINAL) - self.git("add", ".") - self.git("commit", "-qm", "docs: record technical debt") def tearDown(self): self.directory.cleanup() def git(self, *args, check=True): return subprocess.run( - ["git", "-c", "user.name=Debt Test", "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + ["git", "-c", "user.name=Debt Test", "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", + "-c", "maintenance.auto=false", *args], cwd=self.root, capture_output=True, text=True, check=check, env={**os.environ, "GIT_EDITOR": "true"}, ) @@ -68,10 +67,19 @@ class DebtSplitTests(unittest.TestCase): return subprocess.run([sys.executable, str(TOOL), command], cwd=self.root, capture_output=True, text=True, timeout=20) def split(self): - result = self.tool("split") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + directory, streams = os.getcwd(), (sys.stdout, sys.stderr) + os.chdir(self.root) + try: + with open(os.devnull, "w", encoding="utf-8") as sink: + sys.stdout = sys.stderr = sink + debt_split.main(["split"]) + finally: + os.chdir(directory) + sys.stdout, sys.stderr = streams + + def commit(self, message): self.git("add", "-A") - self.git("commit", "-qm", "docs: split technical debt") + self.git("commit", "-qm", message) def test_split_preserves_external_urls_and_rewrites_local_links(self): links = ["HTTPS://example.test/guide#intro", "//example.test/guide#intro", @@ -114,11 +122,13 @@ class DebtSplitTests(unittest.TestCase): self.assertEqual(self.read(INDEX).count("[debt](debt/)"), 1) def rebase(self, stubs, functional): + self.commit("docs: record technical debt") self.git("checkout", "-qb", "change") self.write(INDEX, index(stubs, functional)) self.git("commit", "-qam", "feat: change debt") self.git("checkout", "-q", "main") self.split() + self.commit("docs: split technical debt") self.git("checkout", "-q", "change") rebase = self.git("rebase", "main", check=False) self.assertNotEqual(rebase.returncode, 0, rebase.stdout + rebase.stderr) @@ -170,11 +180,13 @@ class DebtSplitTests(unittest.TestCase): self.assertIn("<<<<<<<", self.read(INDEX)) def test_resolve_refuses_an_entry_main_changed_too(self): + self.commit("docs: record technical debt") self.git("checkout", "-qb", "change") self.write(INDEX, index([A_STUB, B_STUB + " here", C_STUB], [A_FUNCTIONAL, SHADER, SHARED])) self.git("commit", "-qam", "feat: change b") self.git("checkout", "-q", "main") self.split() + self.commit("docs: split technical debt") path = "docs/dev/debt/libSceB.md" self.write(path, self.read(path).replace("returns 0", "returns 1")) self.git("commit", "-qam", "docs: change b") @@ -185,6 +197,7 @@ class DebtSplitTests(unittest.TestCase): self.assertIn("entry not found", result.stderr) def test_resolve_needs_a_conflict(self): + self.commit("docs: record technical debt") result = self.tool("resolve") self.assertNotEqual(result.returncode, 0) self.assertIn("not in a three-way conflict", result.stderr) @@ -211,6 +224,69 @@ class DebtSplitTests(unittest.TestCase): "\n\n### Functional\n\n" + "\n".join(entry.replace("../../", "../../../") for entry in (added, A_FUNCTIONAL)) + "\n") + def test_resolve_preserves_an_entry_moved_before_another_in_its_file(self): + first = "- [libSceA](../../core/libs/prx/libSceA/First.cpp) - first limitation" + second = "- [libSceA](../../core/libs/prx/libSceA/Second.cpp) - second limitation" + self.write(INDEX, index([first, second], [])) + self.git("add", "--", INDEX) + self.git("commit", "-qm", "docs: record ordered debt") + self.git("checkout", "-qb", "reorder") + self.write(INDEX, index([second, first], [])) + self.git("commit", "-qam", "docs: reorder debt") + self.git("checkout", "-q", "main") + result = self.tool("split") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.git("add", "-A") + self.git("commit", "-qm", "docs: split technical debt") + self.git("checkout", "-q", "reorder") + result = self.git("rebase", "main", check=False) + self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr) + result = self.tool("resolve") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.git("rebase", "--continue") + self.assertEqual(self.read("docs/dev/debt/libSceA.md"), + "# libSceA technical debt\n\n### Silent stubs\n\n" + + "\n".join(entry.replace("../../", "../../../") for entry in (second, first)) + "\n") + + def test_apply_changes_preserve_original_occurrences_and_upstream_entries(self): + a = "- [libSceA](../../core/libs/prx/libSceA/A.cpp) - a" + b = "- [libSceA](../../core/libs/prx/libSceA/B.cpp) - b" + c = "- [libSceA](../../core/libs/prx/libSceA/C.cpp) - c" + u, v = "- upstream first", "- upstream last" + cases = ( + ([a, b], [a, b], [b, a], [b, a]), + ([a, b, c], [a, u, b, c, v], [c, a, b], [c, a, u, b, v]), + ([a, b, a], [a, u, b, a, v], [a, a, b], [a, a, u, b, v]), + ([a, b, c, a], [a, u, b, c, a, v], [a, c, a, b], [a, c, a, u, b, v]), + ([a, b, a], [a, u, b, a, v], [b, a], [u, b, a, v]), + ([a, b, a], [a, u, b, a, v], [a, b], [a, u, b, v]), + ([a, b, a], [a, u, b, a, v], [a, b, a + " revised"], [a, u, b, a + " revised", v]), + ([a, b, a], [a, u, b, a, v], [a, b, a, c], [a, u, b, a, c, v]), + ([a, b], [a, u, b, v], [a, c, c, b], [a, c, c, u, b, v]), + ([a, b], [a, u, b, v], [], [u, v]), + ([a, b, a], [a, u, b, a, v], [a + " revised", a, c], [a + " revised", a, u, c, v]), + ([a, b], [a, u, b + " upstream", v], [a + " revised", b], [a + " revised", u, b + " upstream", v]), + ) + path = "docs/dev/debt/libSceA.md" + for before, upstream, after, expected in cases: + with self.subTest(before=before, after=after): + self.write(path, "# libSceA technical debt\n\n### Functional\n\n" + + "\n".join(entry.replace("../../", "../../../") for entry in upstream) + "\n") + debt_split.apply_change(self.root, index([], before), index([], after), index([], [])) + self.assertEqual(self.read(path), "# libSceA technical debt\n\n### Functional\n\n" + + "\n".join(entry.replace("../../", "../../../") for entry in expected) + "\n") + + def test_apply_changes_missing_original_does_not_write_files(self): + original = "# libSceA technical debt\n\n### Silent stubs\n\n" + A_STUB.replace("../../", "../../../") + "\n" + a_path, b_path = "docs/dev/debt/libSceA.md", "docs/dev/debt/libSceB.md" + self.write(a_path, original) + self.write(b_path, "# libSceB technical debt\n\n### Silent stubs\n\n- changed upstream\n") + before = {path: self.read(path) for path in (a_path, b_path)} + with self.assertRaises(LookupError): + debt_split.apply_change(self.root, index([A_STUB, B_STUB], []), + index([A_STUB + " revised", B_STUB + " revised"], []), index([], [])) + self.assertEqual({path: self.read(path) for path in (a_path, b_path)}, before) + def test_indented_continuation_after_a_blank_line_moves_with_its_entry(self): note = " - sub note about sceAOne" self.write(INDEX, "# Project technical debt\n\n### Silent stubs\n\n" + A_STUB + "\n\n" + note + "\n") diff --git a/tools/tests/test_env_reference.py b/tools/tests/test_env_reference.py index c19bb714c9..3b43421864 100644 --- a/tools/tests/test_env_reference.py +++ b/tools/tests/test_env_reference.py @@ -169,6 +169,19 @@ class EnvReferenceTests(unittest.TestCase): self.assertTrue([name for name in found if name.startswith("APS5_")]) self.assertTrue([name for name in found if name.startswith("ANYPS5_")]) + def test_reads_fails_on_a_source_file_it_cannot_decode(self): + root = self.fixture() + (root / "core" / "Latin.cpp").write_bytes(b'static const char* s = "caf\xe9";\n') + with self.assertRaises(ValueError) as raised: + env_reference.reads([root / "core"], BOTH) + self.assertIn("Latin.cpp", str(raised.exception)) + stderr = io.StringIO() + with contextlib.redirect_stderr(stderr): + code = env_reference.main(["--check", "--doc", str(root / "missing.md"), "--root", str(root / "core")]) + self.assertEqual(code, 1) + self.assertIn("cannot read", stderr.getvalue()) + self.assertIn("Latin.cpp", stderr.getvalue()) + def test_check_passes_when_every_read_name_has_a_row(self): root = self.fixture() found = env_reference.reads([root / "core"]) diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index c1acc164c6..5554152a68 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -104,6 +104,13 @@ def exporting(*names): return build_elf([(name, 1, GLOBAL_FUNCTION) for name in names]) +def elf_dynamic_range_fields(image): + sections = struct.unpack_from(" 1) NotImplemented_nid_no_patch(__func__); return value; }\n" + "int APS5_VABI sceBlock(int value) { if (value) { NotImplemented_nid_no_patch(__func__); } return 0; }\n" + "int APS5_VABI sceElse(int value) { if (value) return 1; else Fail(); return 0; }\n" + "int APS5_VABI sceGuarded(int value) { Check(value); return value; }\n" + "int APS5_VABI sceLoop(int value) { for (int i = 0; i < value; ++i) Fail(); return 0; }\n" + "int APS5_VABI sceSwitch(int value) { switch (value) { case 0: return 0; default: Fail(); } return 1; }\n" + "int APS5_VABI sceTernary(int value) { return value ? value : NotImplemented_nid_no_patch(__func__); }\n" + "int APS5_VABI sceShort(int value) { return value && NotImplemented_nid_no_patch(__func__); }\n" + "int APS5_VABI sceLambda(int value) { auto f = [&]() { Fail(); }; return value; }\n" + "int APS5_VABI sceMember(int value) { if (value) Fail(); other.Fail(); return value; }\n" + "int APS5_VABI sceHost(int value) {\n#if defined(_WIN32)\n return value;\n#elif defined(__linux__)\n" + " return value + 1;\n#else\n NotImplemented_nid_no_patch(__func__);\n return 0;\n#endif\n}\n" + "int APS5_VABI sceHostHelper(int value) {\n#ifdef HOST_WITHOUT_IT\n Fail();\n#endif\n return value;\n}\n" + "#ifdef ONLY_HERE\nint APS5_VABI sceOuter(int value) { NotImplemented_nid_no_patch(__func__); return 0; }\n#endif\n" + "int APS5_VABI sceEveryHost(int value) {\n#if defined(_WIN32)\n NotImplemented_nid_no_patch(__func__);\n#else\n" + " NotImplemented_nid_no_patch(__func__);\n#endif\n return 0;\n}\n" + "int APS5_VABI sceEveryElif(int value) {\n#if defined(HOST_A)\n Fail();\n#elif defined(HOST_B)\n" + " NotImplemented_nid_no_patch(__func__);\n#else\n Fail();\n#endif\n return 0;\n}\n" + "int APS5_VABI sceNested(int value) {\n#if defined(HOST_A)\n#ifdef FEATURE\n return value;\n#else\n Fail();\n#endif\n" + "#else\n Fail();\n#endif\n return 0;\n}\n" + "void SoftFail(int value) { if (value == 0) return; NotImplemented_nid_no_patch(__func__); }\n" + "void HardFail(int value) { if (value == 0) throw std::invalid_argument(\"zero\"); NotImplemented_nid_no_patch(__func__); }\n" + "int APS5_VABI sceGuardReturn(int value) { if (value == 0) return 0; NotImplemented_nid_no_patch(__func__); return 1; }\n" + "int APS5_VABI sceGuardThrow(int value) { if (value == 0) throw std::invalid_argument(\"zero\"); " + "NotImplemented_nid_no_patch(__func__); return 1; }\n" + "int APS5_VABI sceGuardBlock(int value) { { if (value == 0) return 0; } Fail(); return 1; }\n" + "int APS5_VABI sceGuardGoto(int value) { if (value == 0) goto done; Fail(); done: return 0; }\n" + "int APS5_VABI sceGuardHost(int value) {\n#ifdef HOST_A\n if (value == 0) return 0;\n#endif\n Fail();\n return 1;\n}\n" + "int APS5_VABI sceSoftHelper(int value) { SoftFail(value); return 0; }\n" + "int APS5_VABI sceHardHelper(int value) { HardFail(value); return 0; }\n" + "int APS5_VABI scePlainBlock(int value) { { NotImplemented_nid_no_patch(__func__); } return 0; }\n" + "int APS5_VABI sceNestedBlock(int value) { value += 1; { { Fail(); } } return value; }\n" + "int APS5_VABI sceBlockThenGuard(int value) { { if (value == 0) return 0; NotImplemented_nid_no_patch(__func__); } return 1; }\n") + write_source(source, "libShared", '#include "prx/libGuard/Export.cpp"\n') + stubs, partial = import_audit.source_stub_classes(source) + names = ("sceDirect", "sceReturned", "sceWrapped", "sceIf", "sceBlock", "sceElse", "sceGuarded", + "sceLoop", "sceSwitch", "sceTernary", "sceShort", "sceLambda", "sceMember", "sceHost", "sceHostHelper", + "sceOuter", "sceEveryHost", "sceEveryElif", "sceNested", "sceGuardReturn", "sceGuardThrow", + "sceGuardBlock", "sceGuardGoto", "sceGuardHost", "sceSoftHelper", "sceHardHelper", + "scePlainBlock", "sceNestedBlock", "sceBlockThenGuard") + kinds = {name: "partial" if ("libGuard.prx", nid_names.compute_nid(name)) in partial else "stub" for name in names} + self.assertEqual({name for name, kind in kinds.items() if kind == "stub"}, + {"sceDirect", "sceReturned", "sceWrapped", "sceOuter", "sceEveryHost", "sceEveryElif", + "sceGuardThrow", "sceHardHelper", "scePlainBlock", "sceNestedBlock"}) + self.assertEqual(set(stubs), {(library, nid_names.compute_nid(name)) for library in ("libGuard.prx", "libShared.prx") for name in names}) + self.assertEqual(partial, {(library, nid_names.compute_nid(name)) for library in ("libGuard.prx", "libShared.prx") + for name, kind in kinds.items() if kind == "partial"}) + + def test_partial_import_is_its_own_class(self): + direct, guarded = nid_names.compute_nid("sceDirect"), nid_names.compute_nid("sceGuarded") + stubs = {("libA.prx", direct): "sceDirect", ("libA.prx", guarded): "sceGuarded", ("libB.prx", guarded): "sceGuarded"} + partial = {("libB.prx", guarded)} + exports = {direct: ["libA.prx"], guarded: ["libA.prx", "libB.prx"]} + imports = [(direct, "libA.prx", False), (guarded, "libA.prx", False), (guarded, "libA.prx", False)] + records, _ = import_audit.audit(imports, exports, stubs, {"libA.prx", "libB.prx"}, set(), partial=partial) + self.assertEqual({record["nid"]: (record["class"], record["name"]) for record in records}, + {direct: ("stub", "sceDirect"), guarded: ("partial", "sceGuarded")}) + summary = import_audit.summarize(records, 3, {}) + self.assertEqual(summary["references_by_class"], {"implemented": 0, "partial": 2, "stub": 1, "absent": 0, "module": 0}) + text = import_audit.render(summary, records, {}, []) + self.assertIn("Stub: exported but throws when called (1)", text) + self.assertIn("Partial: exported, throws only for inputs whose behaviour is unknown (1)", text) + def test_shared_source_cycle_fails(self): source = self.work.source for library, owner in (("libA", "libB"), ("libB", "libA")): @@ -1539,6 +1720,61 @@ class ImportAuditTests(unittest.TestCase): with self.assertRaisesRegex(import_audit.AuditError, "lib.prx"): import_audit.pe_exports(path, image[:cut]) + def test_pe_without_an_export_directory_has_no_exports(self): + for magic, directory in ((0x10b, 96), (0x20b, 112)): + image = bytearray(build_pe([])) + struct.pack_into("= count: + raise ValueError('shard index must be within the shard count') + tests = sorted(iter_test_cases(unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__])), key=lambda test: test.id()) + return unittest.TestSuite(test for position, test in enumerate(tests) if position % count == index) + + +class ShardTests(unittest.TestCase): + def test_shards_run_each_case_once(self): + discovered = sorted(test.id() for test in iter_test_cases(unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__]))) + for count in range(1, len(discovered) + 2): + with self.subTest(count=count): + sharded = [test.id() for index in range(count) for test in iter_test_cases(shard_suite(index, count))] + self.assertEqual(sorted(sharded), discovered) + + def test_invalid_shard_is_rejected(self): + for index, count in ((0, 0), (-1, 2), (2, 2)): + with self.subTest(index=index, count=count), self.assertRaises(ValueError): + shard_suite(index, count) + + if __name__ == '__main__': + if len(sys.argv) > 1 and sys.argv[1] == '--shard': + if len(sys.argv) != 3 or '/' not in sys.argv[2]: + print('usage: test_shader_cache_runtime_sources.py [SOURCE CMAKE] --shard INDEX/COUNT', file=sys.stderr) + raise SystemExit(2) + try: + shard_index, shard_count = (int(part) for part in sys.argv[2].split('/', 1)) + suite = shard_suite(shard_index, shard_count) + except ValueError as error: + print(f'invalid shard: {error}', file=sys.stderr) + raise SystemExit(2) + raise SystemExit(0 if unittest.TextTestRunner().run(suite).wasSuccessful() else 1) unittest.main() diff --git a/tools/tests/test_stubgen.py b/tools/tests/test_stubgen.py index eedd1e3a09..41fab273fb 100644 --- a/tools/tests/test_stubgen.py +++ b/tools/tests/test_stubgen.py @@ -42,6 +42,34 @@ class StubgenTests(unittest.TestCase): class StubGeneratorTests(unittest.TestCase): + def test_long_output_names(self): + import subprocess + with tempfile.TemporaryDirectory() as directory: + root = Path(directory).resolve() + if os.name == "nt": + root = Path("\\\\?\\" + str(root)) + limit = os.pathconf(root, "PC_NAME_MAX") if os.name == "posix" else 255 + nids = root / "nids.txt" + nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") + expected = stubgen.generate("libSceExample", ["AAAAAAAAAAA"], {}) + for option, extra, content in (("--out", [], expected[0]), + ("--td-out", ["--td-only"], expected[1])): + for existing in (False, True): + with self.subTest(option=option, existing=existing): + output = root / ("a" * (limit - 4) + ".txt") + output.write_bytes(b"previous output\n") + if not existing: + output.unlink() + result = subprocess.run( + [sys.executable, str(Path(stubgen.__file__)), "--module", "libSceExample", + "--nids", str(nids), option, str(output), *extra], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(output.read_text(encoding="utf-8"), content) + self.assertEqual(nids.read_text(encoding="utf-8"), "AAAAAAAAAAA\n") + self.assertEqual(set(root.iterdir()), {nids, output}) + output.unlink() + @unittest.skipUnless(os.name == "nt", "Windows denies replacing read-only output files") def test_readonly_second_output_preserves_both_outputs(self): import subprocess diff --git a/tools/tests/test_suggest_patches.py b/tools/tests/test_suggest_patches.py index 0da25c6f5d..f24f84b86d 100644 --- a/tools/tests/test_suggest_patches.py +++ b/tools/tests/test_suggest_patches.py @@ -2,6 +2,7 @@ import contextlib import hashlib import io import json +import os import subprocess import sys import tempfile @@ -63,6 +64,58 @@ class PatchAdvisorTests(unittest.TestCase): str(self.input)]) self.assertFalse((self.root / "relinker-patches.toml").exists()) + def test_relinker_output_is_utf8_independent_of_host_encoding(self): + actual_run = subprocess.run + for diagnostic in ("caf\u00e9 \u65e5\u672c\u8a9e".encode(), b"invalid byte \xff"): + for status in (0, 2): + with self.subTest(diagnostic=diagnostic, status=status): + stdout = VERIFIED.encode() + diagnostic + b"\n" + stderr = diagnostic + b"\n" + captured = [] + output, errors = io.StringIO(), io.StringIO() + + def verify(command, **options): + script = f"import os; os.write(1, {stdout!r}); os.write(2, {stderr!r}); raise SystemExit({status})" + options.setdefault("encoding", "ascii") + result = actual_run([sys.executable, "-c", script], **options) + captured.append(result) + return result + + with contextlib.redirect_stdout(output), contextlib.redirect_stderr(errors), \ + patch.object(suggest_patches.subprocess, "run", side_effect=verify): + self.assertEqual(self.advise(), status) + self.assertEqual(captured[0].stdout, stdout.decode("utf-8", errors="replace")) + self.assertEqual(captured[0].stderr, stderr.decode("utf-8", errors="replace")) + if status == 2: + self.assertIn("relinker error (exit 2)", output.getvalue()) + self.assertIn((stdout + stderr).decode("utf-8", errors="replace").strip(), output.getvalue()) + self.assertIn("No recommendation: relinker errors occurred", errors.getvalue()) + + @patch.object(suggest_patches.subprocess, "run") + def test_output_streams_use_utf8(self, run): + catalog = self.root / "caf\u00e9" + self.catalog.rename(catalog) + self.catalog = catalog + self.param = self.root / "caf\u00e9.json" + self.param.write_text(json.dumps({"titleId": "TEST00001", "contentVersion": "01.000.000"})) + stdout = io.TextIOWrapper(io.BytesIO(), encoding="ascii") + stderr = io.TextIOWrapper(io.BytesIO(), encoding="ascii") + self.addCleanup(stdout.close) + self.addCleanup(stderr.close) + run.return_value = subprocess.CompletedProcess([], 0, VERIFIED, "") + with patch.object(suggest_patches.sys, "stdout", stdout), patch.object(suggest_patches.sys, "stderr", stderr): + self.assertEqual(self.advise(), 0) + stdout.flush() + self.assertIn(b"Recommended:", stdout.buffer.getvalue()) + self.assertIn(str(self.catalog).encode("utf-8"), stdout.buffer.getvalue()) + self.param.unlink() + self.assertEqual(self.advise(), 2) + stderr.flush() + self.assertIn(self.param.name.encode("utf-8"), stderr.buffer.getvalue()) + for stream in (stdout, stderr): + self.assertEqual(stream.encoding, "utf-8") + self.assertEqual(stream.errors, "backslashreplace") + @patch.object(suggest_patches.subprocess, "run") def test_changed_module_is_not_sent_to_relinker(self, run): self.module.write_bytes(b"module version two") @@ -189,9 +242,80 @@ class PatchAdvisorTests(unittest.TestCase): output = io.StringIO() with contextlib.redirect_stdout(output): self.assertEqual(self.advise(), 1) - self.assertIn("multiple verified catalog entries", output.getvalue()) + self.assertIn("multiple verified catalog entries match; use --list-verified to see them.", output.getvalue()) self.assertEqual(run.call_count, 2) + @patch.object(suggest_patches.subprocess, "run") + def test_list_verified_shows_each_match_and_quoted_patch_path(self, run): + second = self.catalog / "second patch.toml" + second.write_bytes((self.catalog / "patch.toml").read_bytes()) + self.write_index([self.entry, dict(self.entry, patch=second.name)]) + run.return_value.returncode = 0 + run.return_value.stdout = VERIFIED + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(self.advise("--list-verified"), 0) + self.assertIn("Verified catalog entries: 2", output.getvalue()) + self.assertIn("Choose one patch file; listed entries are not combined.", output.getvalue()) + self.assertIn(f"Catalog entry 1: {self.catalog / 'patch.toml'}", output.getvalue()) + self.assertIn(f"Catalog entry 2: {second}", output.getvalue()) + quoted = f'"{second}"' if os.name == "nt" else f"'{second}'" + self.assertIn(f"--patches {quoted} --module-dir Media/Modules", output.getvalue()) + self.assertEqual(output.getvalue().count("SHA-256"), 2) + self.assertNotIn("Recommended:", output.getvalue()) + self.assertEqual(run.call_count, 2) + + @patch.object(suggest_patches.subprocess, "run") + def test_list_verified_single_match_preserves_conversion_options(self, run): + self.additional_executable() + run.return_value.returncode = 0 + run.return_value.stdout = VERIFIED + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(self.advise("--list-verified", "--executable", "picker/picker.bin", + "--sce-module-path", str(self.root)), 0) + expected = suggest_patches.command_line(["--patches", str(self.catalog / "patch.toml"), + "--sce-module-path", str(self.root), "--module-dir", + "Media/Modules", "--executable", "picker/picker.bin"]) + self.assertIn(expected, output.getvalue()) + self.assertIn("Verified catalog entries: 1", output.getvalue()) + self.assertIn("Choose one patch file; listed entries are not combined.", output.getvalue()) + self.assertNotIn("Recommended:", output.getvalue()) + + @patch.object(suggest_patches.subprocess, "run") + def test_list_verified_skips_individual_errors(self, run): + bad = dict(self.entry, patch="missing.toml") + wrong_hash = dict(self.entry, targets={"Media/Modules/module.prx": "0" * 64}) + self.write_index([bad, wrong_hash, self.entry, self.entry, self.entry]) + run.side_effect = [subprocess.CompletedProcess([], 2, "", "FAIL: invalid patch"), + subprocess.CompletedProcess([], 1, "Verified 1 patches: 0 ok, 0 not-checked, 1 mismatch\n", ""), + subprocess.CompletedProcess([], 0, VERIFIED, "")] + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(self.advise("--list-verified"), 0) + self.assertIn("Catalog entry 1 ('missing.toml'): rejected", output.getvalue()) + self.assertIn("Catalog entry 2 ('patch.toml'): rejected: Media/Modules/module.prx: SHA-256 mismatch", output.getvalue()) + self.assertIn("Catalog entry 3 ('patch.toml'): relinker error (exit 2)", output.getvalue()) + self.assertIn("Catalog entry 4 ('patch.toml'): rejected by relinker", output.getvalue()) + self.assertIn("Verified catalog entries: 1", output.getvalue()) + self.assertIn(f"Catalog entry 5: {self.catalog / 'patch.toml'}", output.getvalue()) + self.assertEqual(run.call_count, 3) + + @patch.object(suggest_patches.subprocess, "run") + def test_list_verified_no_match_keeps_exit_statuses(self, run): + self.write_index([]) + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(self.advise("--list-verified"), 1) + self.assertIn("No verified catalog entries: no catalog entry", output.getvalue()) + run.assert_not_called() + self.write_index() + run.return_value = subprocess.CompletedProcess([], 2, "", "FAIL: invalid patch") + errors = io.StringIO() + with contextlib.redirect_stderr(errors): + self.assertEqual(self.advise("--list-verified"), 2) + self.assertIn("No verified catalog entries: relinker errors occurred", errors.getvalue()) + @patch.object(suggest_patches.subprocess, "run") def test_not_checked_patch_is_not_recommended(self, run): run.return_value.returncode = 0 diff --git a/tools/tests/test_symbolize_samples.py b/tools/tests/test_symbolize_samples.py new file mode 100644 index 0000000000..2393d66480 --- /dev/null +++ b/tools/tests/test_symbolize_samples.py @@ -0,0 +1,234 @@ +import contextlib +import io +import struct +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import symbolize_samples + +TIMESTAMP = 0x6AD92AC4 +SIZE_OF_IMAGE = 0x3000 + + +def symbol(name, value, section, kind, storage, aux=0): + return struct.pack("<8sIhHBB", name, value, section, kind, storage, aux) + + +def build_pe(path, with_symbols=True, with_exports=False, second_code=False): + image = bytearray(0xC00) + image[0:2] = b"MZ" + struct.pack_into(" [--verbose] """ import argparse +import io import math import secrets import struct @@ -185,6 +186,12 @@ def place_segments(data, headers, records): if header["type"] == PT_LOAD: continue if header["index"] in placed: + host = load_for(header["vaddr"], header["size"]) + if host is not None: + offset = header["vaddr"] - host["vaddr"] + if (host["size"] and offset <= host["size"] and header["size"] <= host["size"] - offset + and header["source"] == host["source"] + offset): + header["host"] = host if header["size"] and (tail is None or header["offset"] > tail[0]): tail = (header["offset"], header["source"] - header["offset"]) continue @@ -217,7 +224,7 @@ def place_segments(data, headers, records): for header in headers: if header["type"] == PT_LOAD and header["size"] == 0: continue - if header["source"] > len(data) or header["size"] > len(data) - header["source"]: + if header["source"] < 0 or header["source"] > len(data) or header["size"] > len(data) - header["source"]: raise Refused("segment %d does not fit the file" % header["index"]) @@ -329,7 +336,7 @@ def unwrap(path, output, verbose=False): destination.parent.mkdir(parents=True, exist_ok=True) temporary_path = destination.parent / (".unwrap-self-" + secrets.token_hex(16)) - temporary = temporary_path.open("xb") + temporary = io.open(temporary_path, "xb") try: with temporary: temporary.write(result)